Skip to content
Commits on Source (9)
......@@ -3,9 +3,8 @@ env:
global:
# Set defaults to avoid repeating in most cases
- PYTHON_VERSION=$TRAVIS_PYTHON_VERSION
- NUMPY_VERSION=stable
- MAIN_CMD='python setup.py'
- CONDA_DEPENDENCIES='sphinx pillow pyproj coveralls coverage mock aggdraw six pyshp'
- CONDA_DEPENDENCIES='sphinx pillow pyproj coveralls coverage mock aggdraw six pyshp pyresample'
- PIP_DEPENDENCIES=''
- SETUP_XVFB=False
- EVENT_TYPE='push pull_request'
......@@ -14,9 +13,13 @@ env:
- CONDA_CHANNEL_PRIORITY='True'
matrix:
include:
- env: PYTHON_VERSION=2.7
- env:
- PYTHON_VERSION=2.7
- NUMPY_VERSION=1.16
os: linux
- env: PYTHON_VERSION=2.7
- env:
- PYTHON_VERSION=2.7
- NUMPY_VERSION=1.16
os: osx
- env: PYTHON_VERSION=3.6
os: linux
......@@ -25,6 +28,13 @@ matrix:
- env: PYTHON_VERSION=3.6
os: windows
language: bash
- env: PYTHON_VERSION=3.7
os: linux
- env: PYTHON_VERSION=3.7
os: osx
- env: PYTHON_VERSION=3.7
os: windows
language: bash
install:
- git clone --depth 1 git://github.com/astropy/ci-helpers.git
- source ci-helpers/travis/setup_conda.sh
......
## Version 1.3.0 (2019/10/25)
### Issues Closed
* [Issue 29](https://github.com/pytroll/pycoast/issues/29) - pycoast compatability issue with pyproj 2+
* [Issue 26](https://github.com/pytroll/pycoast/issues/26) - inconsitency adding lat labelling on the right (on graticules)... ([PR 33](https://github.com/pytroll/pycoast/pull/33))
In this release 2 issues were closed.
### Pull Requests Merged
#### Bugs fixed
* [PR 33](https://github.com/pytroll/pycoast/pull/33) - Fix #26 ([26](https://github.com/pytroll/pycoast/issues/26))
#### Features added
* [PR 32](https://github.com/pytroll/pycoast/pull/32) - Add dict configuration
* [PR 30](https://github.com/pytroll/pycoast/pull/30) - Convert to RGBA mode when opening the image for adding coastlines or rivers to file.
* [PR 25](https://github.com/pytroll/pycoast/pull/25) - Add coordinate grid overlaying from configuration file
In this release 4 pull requests were closed.
## Version 1.2.3 (2019/06/06)
### Issues Closed
......@@ -442,5 +466,3 @@ In this release 3 pull requests were closed.
- First version. [Esben S. Nielsen]
- First version. [Esben S. Nielsen]
pycoast (1.2.3+dfsg-3) UNRELEASED; urgency=medium
pycoast (1.3.0+dfsg-1) unstable; urgency=medium
[ Bas Couwenberg ]
* Bump Standards-Version to 4.4.1, no changes.
-- Bas Couwenberg <sebastic@debian.org> Mon, 30 Sep 2019 19:44:38 +0200
[ Antonio Valentino ]
* New upstream release.
* debian/patches:
- refresh all patches
* debian/control:
- bump debhelper from old 11 to 12
- update test dependencies (add python3-pyresample)
- explicitly set Rules-Requires-Root: no
* Remove obsolete fields Name from debian/upstream/metadata.
-- Antonio Valentino <antonio.valentino@tiscali.it> Tue, 29 Oct 2019 07:44:24 +0100
pycoast (1.2.3+dfsg-2) unstable; urgency=medium
......
......@@ -2,14 +2,16 @@ Source: pycoast
Maintainer: Debian GIS Project <pkg-grass-devel@lists.alioth.debian.org>
Uploaders: Antonio Valentino <antonio.valentino@tiscali.it>
Section: python
Rules-Requires-Root: no
Priority: optional
Build-Depends: debhelper (>= 11),
Build-Depends: debhelper-compat (= 12),
dh-python,
python3-all,
python3-aggdraw,
python3-numpy,
python3-pil,
python3-pyproj,
python3-pyresample,
python3-pyshp,
python3-setuptools,
python3-six,
......
......@@ -7,10 +7,10 @@ Subject: Skip tests that use shapes
1 file changed, 2 insertions(+)
diff --git a/pycoast/tests/test_pycoast.py b/pycoast/tests/test_pycoast.py
index 976bb61..16e972a 100644
index 98ea288..862b647 100644
--- a/pycoast/tests/test_pycoast.py
+++ b/pycoast/tests/test_pycoast.py
@@ -327,6 +327,7 @@ class TestPIL(TestPycoast):
@@ -357,6 +357,7 @@ class TestPIL(TestPycoast):
self.assertTrue(fft_metric(grid_data, res),
'Writing of nh polygons failed')
......@@ -18,7 +18,7 @@ index 976bb61..16e972a 100644
def test_add_shapefile_shapes(self):
from pycoast import ContourWriterPIL
grid_img = Image.open(os.path.join(os.path.dirname(__file__),
@@ -591,6 +592,7 @@ class TestPILAGG(TestPycoast):
@@ -644,6 +645,7 @@ class TestPILAGG(TestPycoast):
self.assertTrue(fft_metric(grid_data, res),
'Writing of nh polygons failed')
......
---
Bug-Database: https://github.com/pytroll/pycoast/issues
Bug-Submit: https://github.com/pytroll/pycoast/issues/new
Name: Pycoast
Repository: https://github.com/pytroll/pycoast.git
Repository-Browse: https://github.com/pytroll/pycoast
Pycoast from a configuration file
---------------------------------
If you want to run to avoid typing the same options over and over again, of if
If you want to run to avoid typing the same options over and over again, or if
caching is an optimization you want, you can use a configuration file with the
pycoast options you need:
......@@ -9,6 +9,7 @@ pycoast options you need:
[cache]
file=/var/run/satellit/white_overlay
regenerate=False
[coasts]
level=1
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from .version import get_versions
__version__ = get_versions()['version']
del get_versions
from .cw_pil import ContourWriterPIL
from .cw_agg import ContourWriterAGG
from pycoast.cw_base import get_resolution_from_area
from .version import get_versions
__version__ = get_versions()['version']
del get_versions
class ContourWriter(ContourWriterPIL):
"""Writer wrapper for deprecation warning.
......
......@@ -326,7 +326,7 @@ class ContourWriterAGG(ContourWriterBase):
minor_outline='white', minor_width=0.5,
minor_outline_opacity=255, minor_is_tick=True,
lon_placement='tb', lat_placement='lr'):
"""Add a lon-lat grid to an image.
"""Add a lon-lat grid to an image. The resulting image is in 'RGBA' mode.
:Parameters:
image : object
......@@ -361,6 +361,7 @@ class ContourWriterAGG(ContourWriterBase):
"""
image = Image.open(filename)
image = image.convert('RGBA')
self.add_grid(image, area_def, Dlonlat, dlonlat,
font=font, write_text=write_text,
fill=fill, fill_opacity=fill_opacity,
......@@ -417,7 +418,7 @@ class ContourWriterAGG(ContourWriterBase):
level=1, fill=None, fill_opacity=255,
outline='white', width=1, outline_opacity=255,
x_offset=0, y_offset=0):
"""Add coastlines to an image file.
"""Add coastlines to an image file. The resulting image is in 'RGBA' mode.
:Parameters:
filename : str
......@@ -448,6 +449,7 @@ class ContourWriterAGG(ContourWriterBase):
"""
image = Image.open(filename)
image = image.convert('RGBA')
self.add_coastlines(image, area_def, resolution=resolution,
level=level, fill=fill,
fill_opacity=fill_opacity, outline=outline,
......@@ -492,7 +494,7 @@ class ContourWriterAGG(ContourWriterBase):
def add_borders_to_file(self, filename, area_def, resolution='c',
level=1, outline='white', width=1,
outline_opacity=255, x_offset=0, y_offset=0):
"""Add borders to an image file.
"""Add borders to an image file. The resulting image is in 'RGBA' mode.
:Parameters:
image : object
......@@ -518,6 +520,7 @@ class ContourWriterAGG(ContourWriterBase):
"""
image = Image.open(filename)
image = image.convert("RGBA")
self.add_borders(image, area_def, resolution=resolution, level=level,
outline=outline, width=width,
outline_opacity=outline_opacity, x_offset=x_offset,
......@@ -562,7 +565,7 @@ class ContourWriterAGG(ContourWriterBase):
def add_rivers_to_file(self, filename, area_def, resolution='c', level=1,
outline='white', width=1, outline_opacity=255,
x_offset=0, y_offset=0):
"""Add rivers to an image file.
"""Add rivers to an image file. The resulting image is in 'RGBA' mode.
:Parameters:
image : object
......@@ -589,6 +592,7 @@ class ContourWriterAGG(ContourWriterBase):
"""
image = Image.open(filename)
image = image.convert("RGBA")
self.add_rivers(image, area_def, resolution=resolution, level=level,
outline=outline, width=width,
outline_opacity=outline_opacity, x_offset=x_offset,
......
......@@ -20,9 +20,10 @@
import os
import shapefile
import numpy as np
from PIL import Image, ImageFont
from PIL import Image
import pyproj
import logging
import ast
try:
import configparser
......@@ -32,6 +33,37 @@ except ImportError:
logger = logging.getLogger(__name__)
def get_resolution_from_area(area_def):
"""Get the best resolution for an area definition."""
x_size = area_def.width
y_size = area_def.height
prj = Proj(area_def.proj_str)
if prj.is_latlong():
x_ll, y_ll = prj(area_def.area_extent[0], area_def.area_extent[1])
x_ur, y_ur = prj(area_def.area_extent[2], area_def.area_extent[3])
x_resolution = (x_ur - x_ll) / x_size
y_resolution = (y_ur - y_ll) / y_size
else:
x_resolution = ((area_def.area_extent[2] -
area_def.area_extent[0]) /
x_size)
y_resolution = ((area_def.area_extent[3] -
area_def.area_extent[1]) /
y_size)
res = min(x_resolution, y_resolution)
if res > 25000:
return "c"
elif res > 5000:
return "l"
elif res > 1000:
return "i"
elif res > 200:
return "h"
else:
return "f"
class Proj(pyproj.Proj):
"""Wrapper around pyproj to add in 'is_latlong'."""
......@@ -99,7 +131,7 @@ class ContourWriterBase(object):
def _find_line_intercepts(self, xys, size, margins):
"""Finds intercepts of poly-line xys with image boundaries
offset by margins and returns an array of coordintes"""
offset by margins and returns an array of coordinates"""
x_size, y_size = size
def is_in_box(x_y, extents):
......@@ -120,13 +152,13 @@ class ContourWriterBase(object):
xlim1 = margins[0]
ylim1 = margins[1]
xlim2 = x_size - margins[0]
ylim2 = y_size - margins[0]
ylim2 = y_size - margins[1]
# only consider crossing within a box a little bigger than grid
# boundary
search_box = (-10, x_size + 10, -10, y_size + 10)
# loop trought line steps and detect crossings
# loop through line steps and detect crossings
intercepts = []
align_left = 'LC'
align_right = 'RC'
......@@ -168,7 +200,7 @@ class ContourWriterBase(object):
"""
try:
proj4_string = area_def.proj4_string
proj4_string = area_def.proj_str
area_extent = area_def.area_extent
except AttributeError:
proj4_string = area_def[0]
......@@ -234,7 +266,7 @@ class ContourWriterBase(object):
lat_max - shorten_max_lat,
float(lat_max - lat_min) / y_size)
# lin_lats in rather high definition so that it can be used to
# posituion text labels near edges of image...
# position text labels near edges of image...
# perhaps better to find the actual length of line in pixels...
......@@ -253,7 +285,7 @@ class ContourWriterBase(object):
# lons along major lat lines (extended slightly to avoid missing the
# end)
lin_lons = np.arange(lon_min, lon_max + Dlon / 5.0, Dlon / 10.0)
lin_lons = np.linspace(lon_min, lon_max + Dlon / 5.0, max(x_size, y_size) / 5)
# MINOR LINES ######
if not kwargs['minor_is_tick']:
......@@ -298,9 +330,9 @@ class ContourWriterBase(object):
for lon in maj_lons:
# Draw 'minor' tick lines dlat separation along the lon
if kwargs['minor_is_tick']:
tick_lons = np.arange(lon - Dlon / 20.0,
tick_lons = np.linspace(lon - Dlon / 20.0,
lon + Dlon / 20.0,
Dlon / 50.0)
5)
for lat in min_lats:
lonlats = [(x, lat) for x in tick_lons]
......@@ -354,9 +386,9 @@ class ContourWriterBase(object):
for lat in maj_lats:
# Draw 'minor' tick dlon separation along the lat
if kwargs['minor_is_tick']:
tick_lats = np.arange(lat - Dlat / 20.0,
tick_lats = np.linspace(lat - Dlat / 20.0,
lat + Dlat / 20.0,
Dlat / 50.0)
5)
for lon in min_lons:
lonlats = [(lon, x) for x in tick_lats]
index_arrays, is_reduced = \
......@@ -512,7 +544,7 @@ class ContourWriterBase(object):
"""
try:
proj4_string = area_def.proj4_string
proj4_string = area_def.proj_str
area_extent = area_def.area_extent
except AttributeError:
proj4_string = area_def[0]
......@@ -644,22 +676,12 @@ class ContourWriterBase(object):
yield shape
def _finalize(self, draw):
"""Do any need finalization of the drawing
"""
"""Do any need finalization of the drawing."""
pass
def add_overlay_from_config(self, config_file, area_def):
"""Create and return a transparent image adding all the overlays contained in a configuration file.
:Parameters:
config_file : str
Configuration file name
area_def : object
Area Definition of the creating image
"""
def _config_to_dict(self, config_file):
"""Convert a config file to a dict."""
config = configparser.ConfigParser()
try:
with open(config_file, 'r'):
......@@ -673,58 +695,79 @@ class ContourWriterBase(object):
logger.error("Error in %s", str(config_file))
raise
SECTIONS = ['cache', 'coasts', 'rivers', 'borders', 'cities', 'grid']
overlays = {}
for section in config.sections():
if section in SECTIONS:
overlays[section] = {}
for option in config.options(section):
val = config.get(section, option)
try:
overlays[section][option] = ast.literal_eval(val)
except ValueError:
overlays[section][option] = val
return overlays
def add_overlay_from_dict(self, overlays, area_def, cache_epoch=None, background=None):
"""Create and return a transparent image adding all the overlays contained in the `overlays` dict.
:Parameters:
overlays : dict
overlays configuration
area_def : object
Area Definition of the creating image
cache_epoch: seconds since epoch
The latest time allowed for cache the cache file. If the cache file is older than this (mtime),
the cache should be regenerated.
background: pillow image instance
The image on which to write the overlays on. If it's None (default),
a new image is created, otherwise the provide background is use
an change *in place*.
The keys in `overlays` that will be taken into account are:
cache, coasts, rivers, borders, cities, grid
For all of them except `cache`, the items are the same as the corresponding
functions in pycoast, so refer to the docstrings of these functions
(add_coastlines, add_rivers, add_borders, add_grid, add_cities).
For cache, two parameters are configurable: `file` which specifies the directory
and the prefix of the file to save the caches decoration to
(for example /var/run/black_coasts_red_borders), and `regenerate` that can be
True or False (default) to force the overwriting of an already cached file.
"""
# Cache management
cache_file = None
if config.has_section('cache'):
config_file_name, config_file_extention = \
os.path.splitext(config_file)
cache_file = (config.get('cache', 'file') + '_' +
if 'cache' in overlays:
cache_file = (overlays['cache']['file'] + '_' +
area_def.area_id + '.png')
try:
configTime = os.path.getmtime(config_file)
cacheTime = os.path.getmtime(cache_file)
config_time = cache_epoch
cache_time = os.path.getmtime(cache_file)
# Cache file will be used only if it's newer than config file
if configTime < cacheTime:
if ((config_time is not None and config_time < cache_time)
and not overlays['cache'].get('regenerate', False)):
foreground = Image.open(cache_file)
logger.info('Using image in cache %s', cache_file)
if background is not None:
background.paste(foreground, mask=foreground.split()[-1])
return foreground
else:
logger.info("Cache file is not used "
"because config file has changed")
logger.info("Regenerating cache file.")
except OSError:
logger.info("New overlay image will be saved in cache")
x_size = area_def.x_size
y_size = area_def.y_size
foreground = Image.new('RGBA', (x_size, y_size), (0, 0, 0, 0))
logger.info("No overlay image found, new overlay image will be saved in cache.")
# Lines (coasts, rivers, borders) management
prj = Proj(area_def.proj4_string)
if prj.is_latlong():
x_ll, y_ll = prj(area_def.area_extent[0], area_def.area_extent[1])
x_ur, y_ur = prj(area_def.area_extent[2], area_def.area_extent[3])
x_resolution = (x_ur - x_ll) / x_size
y_resolution = (y_ur - y_ll) / y_size
x_size = area_def.width
y_size = area_def.height
if cache_file is None and background is not None:
foreground = background
else:
x_resolution = ((area_def.area_extent[2] -
area_def.area_extent[0]) /
x_size)
y_resolution = ((area_def.area_extent[3] -
area_def.area_extent[1]) /
y_size)
res = min(x_resolution, y_resolution)
foreground = Image.new('RGBA', (x_size, y_size), (0, 0, 0, 0))
if res > 25000:
default_resolution = "c"
elif res > 5000:
default_resolution = "l"
elif res > 1000:
default_resolution = "i"
elif res > 200:
default_resolution = "h"
else:
default_resolution = "f"
default_resolution = get_resolution_from_area(area_def)
DEFAULT = {'level': 1,
'outline': 'white',
......@@ -736,18 +779,9 @@ class ContourWriterBase(object):
'y_offset': 0,
'resolution': default_resolution}
SECTIONS = ['coasts', 'rivers', 'borders', 'cities']
overlays = {}
for section in config.sections():
if section in SECTIONS:
overlays[section] = {}
for option in config.options(section):
overlays[section][option] = config.get(section, option)
is_agg = self._draw_module == "AGG"
# Coasts
# Coasts, rivers, borders
for section, fun in zip(['coasts', 'rivers', 'borders'],
[self.add_coastlines,
self.add_rivers,
......@@ -795,14 +829,63 @@ class ContourWriterBase(object):
font_size, pt_size, outline, box_outline,
box_opacity)
if 'grid' in overlays:
lon_major = float(overlays['grid'].get('lon_major', 10.0))
lat_major = float(overlays['grid'].get('lat_major', 10.0))
lon_minor = float(overlays['grid'].get('lon_minor', 2.0))
lat_minor = float(overlays['grid'].get('lat_minor', 2.0))
font = overlays['grid'].get('font', None)
font_size = int(overlays['grid'].get('font_size', 10))
write_text = overlays['grid'].get('write_text', True)
if isinstance(write_text, str):
write_text = write_text.lower() in ['true', 'yes', '1', 'on']
outline = overlays['grid'].get('outline', 'white')
if isinstance(font, str):
if is_agg:
from aggdraw import Font
font = Font(outline, font, size=font_size)
else:
from PIL.ImageFont import truetype
font = truetype(font, font_size)
fill = overlays['grid'].get('fill', None)
minor_outline = overlays['grid'].get('minor_outline', 'white')
minor_is_tick = overlays['grid'].get('minor_is_tick',
'true').lower() in \
['true', 'yes', '1']
lon_placement = overlays['grid'].get('lon_placement', 'tb')
lat_placement = overlays['grid'].get('lat_placement', 'lr')
self.add_grid(foreground, area_def, (lon_major, lat_major),
(lon_minor, lat_minor),
font=font, write_text=write_text, fill=fill,
outline=outline, minor_outline=minor_outline,
minor_is_tick=minor_is_tick,
lon_placement=lon_placement,
lat_placement=lat_placement)
if cache_file is not None:
try:
foreground.save(cache_file)
except IOError as e:
logger.error("Can't save cache: %s", str(e))
if background is not None:
background.paste(foreground, mask=foreground.split()[-1])
return foreground
def add_overlay_from_config(self, config_file, area_def, background=None):
"""Create and return a transparent image adding all the overlays contained in a configuration file.
:Parameters:
config_file : str
Configuration file name
area_def : object
Area Definition of the creating image
"""
overlays = self._config_to_dict(config_file)
return self.add_overlay_from_dict(overlays, area_def, os.path.getmtime(config_file), background)
def add_cities(self, image, area_def, citylist, font_file, font_size,
ptsize, outline, box_outline, box_opacity, db_root_path=None):
"""Add cities (point and name) to a PIL image object
......@@ -814,7 +897,7 @@ class ContourWriterBase(object):
raise ValueError("'db_root_path' must be specified to use this method")
try:
proj4_string = area_def.proj4_string
proj4_string = area_def.proj_str
area_extent = area_def.area_extent
except AttributeError:
proj4_string = area_def[0]
......
[coasts]
level = 4
resolution = l
outline = white
[grid]
write_text = False
lon_major = 10.0
lat_major = 10.0
lon_minor = 2.0
lat_minor = 2.0
minor_outline = blue
outline = blue
[coasts]
level = 4
resolution = l
outline = white
[grid]
write_text = True
lon_major = 10.0
lat_major = 10.0
lon_minor = 2.0
lat_minor = 2.0
minor_outline = blue
outline = blue
lon_placement = tblr
lat_placement =
font = pycoast/tests/test_data/DejaVuSerif.ttf
font_size = 10
pycoast/tests/dateline_cross.png

24.7 KiB | W: | H:

pycoast/tests/dateline_cross.png

25.1 KiB | W: | H:

pycoast/tests/dateline_cross.png
pycoast/tests/dateline_cross.png
pycoast/tests/dateline_cross.png
pycoast/tests/dateline_cross.png
  • 2-up
  • Swipe
  • Onion skin
pycoast/tests/grid_europe.png

27.4 KiB | W: | H:

pycoast/tests/grid_europe.png

27.9 KiB | W: | H:

pycoast/tests/grid_europe.png
pycoast/tests/grid_europe.png
pycoast/tests/grid_europe.png
pycoast/tests/grid_europe.png
  • 2-up
  • Swipe
  • Onion skin
pycoast/tests/grid_geos.png

16.8 KiB | W: | H:

pycoast/tests/grid_geos.png

17.3 KiB | W: | H:

pycoast/tests/grid_geos.png
pycoast/tests/grid_geos.png
pycoast/tests/grid_geos.png
pycoast/tests/grid_geos.png
  • 2-up
  • Swipe
  • Onion skin