Compare commits

...
4 Commits
15 changed files with 242 additions and 90 deletions
+5 -1
View File
@@ -1,4 +1,6 @@
*~
*.bak
*.pyc
build.local.bat
build.local.sh
Makefile
@@ -36,9 +38,11 @@ README
/DartConfiguration.tcl
/Testing
# html
# docs
/html/dl
/html/robots.txt
/docs/plot/images
/latex
# m4
/m4/libtool.m4
+45
View File
@@ -0,0 +1,45 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
build:
os: "ubuntu-22.04"
tools:
python: "mambaforge-4.10"
jobs:
post_checkout:
# Cancel building pull requests when there aren't changed in the docs directory or YAML file.
# You can add any other files or directories that you'd like here as well,
# like your docs requirements file, or other files that will change your docs build.
#
# If there are no changes (git diff exits with 0) we force the command to return with 183.
# This is a special exit code on Read the Docs that will cancel the build immediately.
- |
if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/5.1 -- docs/ .readthedocs.yaml;
then
exit 183;
fi
- (git --no-pager log --pretty="tformat:%s -- %b" -1 | grep -viq "skip ci") || exit 183
pre_build:
- ./docs/rtd/pre_build.sh
- ./docs/rtd/plot.sh
apt_packages:
- cmake
- ninja-build
- gcc
formats:
- pdf
- epub
conda:
environment: docs/environment.yml
sphinx:
configuration: docs/source/conf.py
fail_on_warning: true
+13
View File
@@ -0,0 +1,13 @@
name: proj
channels:
- conda-forge
dependencies:
- descartes
- geojson
- matplotlib
- shapely < 1.8
- pip
- pip:
- Jinja2 < 3.1
- sphinx < 3
- sphinx-rtd-theme < 1.0
Regular → Executable
+130 -65
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
'''
Plot map data in different projections supported by PROJ.4
Plot map data in different projections supported by PROJ.
Call with:
@@ -41,28 +42,31 @@ or "line". The rest of the inputs are fairly free form.
Change PROJ to path on your local system before running the script.
'''
from __future__ import print_function
from __future__ import division
import argparse
import os
import os.path
import shutil
import sys
import json
import subprocess
import functools
from pathlib import Path
import geojson
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 MultiPolygon, Polygon, box as Box
from shapely.geometry import shape
from shapely.ops import transform
from descartes import PolygonPatch
PROJ = '../../src/proj'
PROJ_LIB = '../../nad'
PROJ_COMMAND = os.environ.get('PROJ_EXE', '../../src/proj')
if not os.path.exists(PROJ_COMMAND):
PROJ = shutil.which('proj')
if PROJ is None:
raise ValueError("specify PROJ_EXE or modify PATH to find proj")
else:
PROJ = PROJ_COMMAND
PROJ_LIB = os.environ.get('PROJ_LIB', '../../nad')
LINE_LOW = 'data/coastline.geojson'
LINE_MED = 'data/coastline50.geojson'
@@ -85,7 +89,7 @@ def interp_coords(coords, tol):
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
other than the specified tolerance, the list will be densified between those two
points. Roughly speaking, a point will be inserted every `tol` between the
points.
@@ -118,7 +122,7 @@ def interp_coords(coords, tol):
xy.append((x[offset:i], y[offset:i]))
# Interpolate between points above tolerance.
n = np.ceil(dsts[i] / tol)
n = int(np.ceil(dsts[i] / tol))
x1 = np.linspace(x[i], x[i + 1], n + 1)
y1 = np.linspace(y[i], y[i + 1], n + 1)
xy.append((x1[:-1], y1[:-1]))
@@ -157,8 +161,14 @@ def project(coordinates, proj_string, in_radians=False):
args = [PROJ, '-b']
args.extend(proj_string.split(' '))
proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
env={'PROJ_LIB': os.path.abspath(PROJ_LIB)})
try:
proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
env={'PROJ_LIB': os.path.abspath(PROJ_LIB)})
except FileNotFoundError as e:
print("'proj' binary not found, set the PROJ_EXE environment variable "
"to point to your local 'proj' binary --%s--" % e)
exit(1)
stdout, _ = proc.communicate(coordinates.tobytes(order='C'))
out = np.frombuffer(stdout, dtype=np.double)
@@ -217,7 +227,10 @@ def resample_polygon(polygon):
'''
Use interp_coords() to resample (multi)polygons.
'''
xy = polygon.exterior.coords.xy
try:
xy = polygon.exterior.coords.xy
except AttributeError: #no xy's
return polygon
ext = interp_coords(xy, 2)
# interiors
rings = []
@@ -226,27 +239,46 @@ def resample_polygon(polygon):
return Polygon(ext, rings)
def plot_with_interruptions(axes, x, y, delta_cut=1e100, **kwargs):
"""
Plot x/y with proper splitting for interrupted projections.
"""
_x = np.atleast_1d(x)
_y = np.atleast_1d(y)
dx = np.nan_to_num(_x[1: ]) - np.nan_to_num(_x[0: -1])
dy = np.nan_to_num(_y[1: ]) - np.nan_to_num(_y[0: -1])
split, = ((np.abs(dx) > delta_cut) | (np.abs(dy) > delta_cut)).nonzero()
split = np.append(split, _x.size - 1)
split += 1
last_part = 0
for part in split:
axes.plot(x[last_part: part],
y[last_part: part],
**kwargs)
last_part = part
def plotproj(plotdef, data, outdir):
'''
Plot map.
'''
fig, axes = plt.subplots()
axes = plt.axes()
bounds = (plotdef['lonmin'], plotdef['latmin'], plotdef['lonmax'], plotdef['latmax'])
for geom in data.filter(bbox=bounds):
temp_pol = shape(geom['geometry'])
box = Box(
plotdef['lonmin'], plotdef['latmin'],
plotdef['lonmax'], plotdef['latmax'])
for feat in data["features"]:
geom = shape(feat["geometry"])
if not geom.intersects(box):
continue
box = Polygon([
(plotdef['lonmin'], plotdef['latmin']),
(plotdef['lonmin'], plotdef['latmax']),
(plotdef['lonmax'], plotdef['latmax']),
(plotdef['lonmax'], plotdef['latmin']),
])
temp_pol = temp_pol.intersection(box)
temp_pol = geom.intersection(box)
if plotdef['type'] == 'poly':
if isinstance(temp_pol, MultiPolygon):
polys = [resample_polygon(polygon) for polygon in temp_pol]
polys = [resample_polygon(polygon) for polygon in temp_pol.geoms]
pol = MultiPolygon(polys)
else:
pol = resample_polygon(temp_pol)
@@ -257,11 +289,15 @@ def plotproj(plotdef, data, outdir):
proj_geom = transform(trans, pol)
if plotdef['type'] == 'poly':
patch = PolygonPatch(proj_geom, fc=COLOR_LAND, zorder=0)
axes.add_patch(patch)
try:
patch = PolygonPatch(proj_geom, fc=COLOR_LAND, zorder=0)
axes.add_patch(patch)
except TypeError:
pass
else:
x, y = proj_geom.xy
axes.plot(x, y, color=COLOR_COAST, linewidth=0.5)
plot_with_interruptions(axes, x, y, color=COLOR_COAST, linewidth=0.5,
delta_cut=plotdef.get('delta_cut', 1e100))
# Plot frame
frame = [
@@ -272,7 +308,8 @@ def plotproj(plotdef, data, outdir):
line = project(line, plotdef['projstring'])
x = line[:, 0]
y = line[:, 1]
axes.plot(x, y, '-k')
plot_with_interruptions(axes, x, y, color='black', linestyle='-',
delta_cut=plotdef.get('delta_cut', 1e6))
graticule = build_graticule(
plotdef['lonmin'],
@@ -286,60 +323,77 @@ def plotproj(plotdef, data, outdir):
feature = project(feature, plotdef['projstring'])
x = feature[:, 0]
y = feature[:, 1]
axes.plot(x, y, color=COLOR_GRAT, linewidth=0.4)
plot_with_interruptions(axes, x, y, color=COLOR_GRAT, linewidth=0.4,
delta_cut=plotdef.get('delta_cut', 1e6))
# Plot interrupted boundaries if necessary
interrupted_lines = []
if 'top_interrupted_lons' in plotdef:
for lon in plotdef['top_interrupted_lons']:
for delta in [-0.0001, 0.0001]:
merid = meridian(lon + delta, 0.0, plotdef['latmax'])
interrupted_lines.append(project(merid, plotdef['projstring']))
if 'bottom_interrupted_lons' in plotdef:
for lon in plotdef['bottom_interrupted_lons']:
for delta in [-0.0001, 0.0001]:
merid = meridian(lon + delta, plotdef['latmin'], 0)
interrupted_lines.append(project(merid, plotdef['projstring']))
for line in interrupted_lines:
x = line[:, 0]
y = line[:, 1]
plot_with_interruptions(axes, x, y, color=COLOR_GRAT, linewidth=0.4,
delta_cut=plotdef.get('delta_cut', 1e100))
# Switch off the axis lines...
plt.axis('off')
# ... and additionally switch off the visibility of the axis lines and
# labels so they can be removed by "bbox_inches='tight'" when saving
axes.get_xaxis().set_visible(False)
axes.get_yaxis().set_visible(False)
axes.axis('off')
font = {
'family': 'serif',
'color': 'black',
'style': 'italic',
'size': 12
}
fig.suptitle(plotdef['projstring'], fontdict=font)
axes.autoscale(tight=True)
if not os.path.exists(outdir):
os.makedirs(outdir)
fig.savefig(outdir + '/' + plotdef['filename'], dpi=300)
# Make sure the plot is not stretched
axes.set_aspect('equal')
outdir = Path(outdir)
if not outdir.exists():
outdir.mkdir(parents=True)
if not outdir.is_dir():
raise OSError("outdir is not a directory")
plt.savefig(outdir / plotdef['filename'],
dpi=400,
bbox_inches='tight')
# Clean up
fig = None
del fig
axes = None
del axes
plt.close()
def main():
def main(plotdefs, outdir, plots=[]):
'''
Main function of plotting script.
Parses json-file with plot setups and runs the plotting
for each plot setup.
'''
plotdefs = json.loads(Path(plotdefs).read_text())
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),
('line', 'low'): geojson.loads(Path(LINE_LOW).read_text()),
('line', 'med'): geojson.loads(Path(LINE_MED).read_text()),
('poly', 'low'): geojson.loads(Path(POLY_LOW).read_text()),
('poly', 'med'): geojson.loads(Path(POLY_MED).read_text()),
}
if os.path.exists(sys.argv[1]):
# first argument is the JSON plot definition setup file
with open(sys.argv[1]) as plotsetup:
plotdefs = json.load(plotsetup)
else:
raise ValueError('No plot definition file entered')
plots = []
# second argument is the output dir
outdir = sys.argv[2]
# subsecond arguments are (optional) names of plot in plotdef.json
if len(sys.argv) > 3:
plots = sys.argv[3:len(sys.argv)]
for i, plotdef in enumerate(plotdefs):
if plots != [] and plotdef['name'] not in plots:
continue
@@ -351,9 +405,20 @@ def main():
plotproj(plotdef, data[(plotdef['type'], plotdef['res'])], outdir)
for key in data:
data[key].close()
if __name__ == "__main__":
sys.exit(main())
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
'plotdefs',
help='A JSON file with setup for each auto-generated plot')
parser.add_argument(
'outdir',
help='Directory to put the plots in.')
parser.add_argument(
'plots', nargs='*',
help='A list of plot names within the plotdefs file. '
'More than one plotname can be given at once.')
args = parser.parse_args()
sys.exit(main(**vars(args)))
-2
View File
@@ -1,2 +0,0 @@
python3 plot.py plotdefs.json images/
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
set -e
export PATH="$HOME/.local/bin:$PATH"
export PROJ_LIB="$HOME/.local/share/proj"
cd docs/plot
python plot.py plotdefs.json images
cd ../..
mkdir -p docs/source/operations/projections/images/
cp -r docs/plot/images/* docs/source/operations/projections/images/
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
set -e
mkdir build
cd build
cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DPROJ_TESTS=OFF -DCMAKE_INSTALL_PREFIX=$HOME/.local ..
ninja install
+17 -12
View File
@@ -14,7 +14,6 @@
import sys
import os
import datetime
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@@ -55,28 +54,26 @@ project = 'PROJ.4'
# See CITATION file
title = 'PROJ coordinate transformation software library'
author = 'PROJ contributors'
now = datetime.datetime.now()
copyright = u'1983-{0}'.format(now.year)
copyright = u'1983-2018'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
version = '5.0.1'
version = '5.1'
# use same |release| as |version|
release = version
release = '5.1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# language = 'en'
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
today = 'June 2018'
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
@@ -152,10 +149,16 @@ html_theme_options = {}
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
htm_css_files = [
'theme_overrides.css', # override wide tables in RTD theme
]
html_context = {
'css_files': [
'_static/theme_overrides.css', # override wide tables in RTD theme
],
'display_github': True,
'theme_vcs_pageview_mode': 'edit',
'github_user': 'OSGeo',
'github_repo': 'PROJ',
'github_version': '/5.1/docs/source/',
}
# Add any extra paths that contain custom files (such as robots.txt or
@@ -228,6 +231,8 @@ preamble = r"""
\ifdefined\DeclareUnicodeCharacter
\DeclareUnicodeCharacter{2032}{$'$}% prime
\fi
\usepackage[notbib,notindex]{tocbibind}
"""
latex_elements = {
@@ -239,7 +244,7 @@ latex_elements = {
# Additional stuff for the LaTeX preamble.
'preamble': preamble,
'inputenc':'\usepackage[utf8]{inputenc}'
'inputenc': r'\usepackage[utf8]{inputenc}',
# Latex figure (float) alignment
#'figure_align': 'htbp',
+4 -4
View File
@@ -13,16 +13,16 @@ distribution of the source code and various resource files archives. See
Current Release
--------------------------------------------------------------------------------
* **2018-04-01** `proj-5.0.1.tar.gz`_ (`md5`_)
* **2018-06-01** `proj-5.1.0.tar.gz`_ (`md5`_)
* **2018-03-01** `proj-datumgrid-1.7.zip`_
* **2018-03-01** `proj-datumgrid-europe-1.0.zip`_
* **2018-03-01** `proj-datumgrid-north-america-1.0.zip`_
* **2018-03-01** `proj-datumgrid-oceania-1.0.zip`_
* **PDF Manual** `proj.pdf`_
Past Releases
--------------------------------------------------------------------------------
* **2018-04-01** `proj-5.0.1.tar.gz`_
* **2018-03-01** `proj-5.0.0.tar.gz`_
* **2016-09-02** `proj-4.9.3.tar.gz`_
* **2015-09-13** `proj-4.9.2.tar.gz`_
@@ -30,6 +30,7 @@ Past Releases
* **2016-09-11** `proj-datumgrid-1.6.zip`_
.. _`proj-5.1.0.tar.gz`: http://download.osgeo.org/proj/proj-5.1.0.tar.gz
.. _`proj-5.0.1.tar.gz`: http://download.osgeo.org/proj/proj-5.0.1.tar.gz
.. _`proj-5.0.0.tar.gz`: http://download.osgeo.org/proj/proj-5.0.0.tar.gz
.. _`proj-4.9.1.tar.gz`: http://download.osgeo.org/proj/proj-4.9.1.tar.gz
@@ -40,5 +41,4 @@ Past Releases
.. _`proj-datumgrid-europe-1.0.zip`: http://download.osgeo.org/proj/proj-datumgrid-europe-1.0.zip
.. _`proj-datumgrid-north-america-1.0.zip`: http://download.osgeo.org/proj/proj-datumgrid-north-america-1.0.zip
.. _`proj-datumgrid-oceania-1.0.zip`: http://download.osgeo.org/proj/proj-datumgrid-oceania-1.0.zip
.. _`md5`: http://download.osgeo.org/proj/proj-5.0.1.tar.gz.md5
.. _`proj.pdf`: https://raw.githubusercontent.com/OSGeo/proj.4/gh-pages/proj.pdf
.. _`md5`: http://download.osgeo.org/proj/proj-5.1.0.tar.gz.md5
+2 -2
View File
@@ -307,14 +307,14 @@ Output of above command:
What options does PROJ allow for the shape of the Earth (geodesy)?
--------------------------------------------------------------------------------
See https://github.com/OSGeo/proj.4/blob/master/src/pj_ellps.c
See https://github.com/OSGeo/proj.4/blob/5.1/src/pj_ellps.c
for possible ellipse options. For example, putting ``+ellps=WGS84`` uses
the ``WGS84`` Earth shape.
What if I want a spherical Earth?
--------------------------------------------------------------------------------
Use ``+ellps=sphere``. See https://github.com/OSGeo/proj.4/blob/master/src/pj_ellps.c
Use ``+ellps=sphere``. See https://github.com/OSGeo/proj.4/blob/5.1/src/pj_ellps.c
for the radius used in this case.
How do I change the radius of the Earth? How do I use PROJ for work on Mars?
+2 -2
View File
@@ -43,12 +43,12 @@
.. [Evenden1995] G. I. Evenden, 1995,
`Cartograpic Projection Procedures for the UNIX Environment -
A User's Manual
<https://github.com/OSGeo/proj.4/blob/master/docs/old/proj_4_3_12.pdf>`_.
<https://github.com/OSGeo/proj.4/blob/5.1/docs/old/proj_4_3_12.pdf>`_.
.. [Evenden2005] G. I. Evenden, 2005,
`libproj4: A Comprehensive Library of Cartographic Projection Functions
(Preliminary Draft)
<https://github.com/OSGeo/proj.4/blob/master/docs/old/libproj.pdf>`_.
<https://github.com/OSGeo/proj.4/blob/5.1/docs/old/libproj.pdf>`_.
.. [EversKnudsen2017] K. Evers and T. Knudsen, 2017,
`Transformation pipelines for PROJ.4
+2 -2
View File
@@ -21,7 +21,7 @@ Longitude wrapping when using custom central meridian
By default PROJ wraps output longitudes in the range -180 to 180. Previous to
PROJ 5, this was handled incorrectly when a custom central meridian was set with
:option:`+lon`. This caused a change in sign on the resulting easting as seen
:option:`+lon_0`. This caused a change in sign on the resulting easting as seen
below::
$ proj +proj=merc +lon_0=110
@@ -43,5 +43,5 @@ The change is made on the basis that :math:`\lambda=290^{\circ}` is a full
rotation of the circle larger than :math:`\lambda=-70^{\circ}` and hence
should return the same output for both.
Adding the :option:`+over` flag to the projection definition provides
Adding the ``+over`` flag to the projection definition provides
the old behaviour.
+1
View File
@@ -2,6 +2,7 @@
./travis/before_install.sh
sudo apt-get update -qq
sudo apt-get install -y cppcheck
set -e