Skip to content

Commit

Permalink
Merge pull request #32 from mraspaud/feature-dict-config
Browse files Browse the repository at this point in the history
Add dict configuration
  • Loading branch information
mraspaud authored Oct 25, 2019
2 parents 795247c + a268464 commit 00c38b8
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 79 deletions.
12 changes: 9 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ 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 pyresample'
- PIP_DEPENDENCIES=''
Expand All @@ -29,9 +28,16 @@ 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
- git clone --depth 1 git://github.com/astropy/ci-helpers.git
- source ci-helpers/travis/setup_conda.sh
script: coverage run --source=pycoast setup.py test
after_success:
- if [[ $PYTHON_VERSION == 3.6 ]]; then coveralls; fi
Expand Down
7 changes: 4 additions & 3 deletions docs/source/config.rst
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
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:

.. code-block:: ini
[cache]
file=/var/run/satellit/white_overlay
regenerate=False
[coasts]
level=1
width=0.75
outline=white
fill=yellow
[borders]
outline=white
width=0.5
Expand Down
10 changes: 5 additions & 5 deletions pycoast/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/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.
Expand All @@ -21,4 +21,4 @@ def __init__(self, *args, **kwargs):
import warnings
warnings.warn("'ContourWriter' has been deprecated please use "
"'ContourWriterPIL' or 'ContourWriterAGG' instead", DeprecationWarning)
super(ContourWriter, self).__init__(*args, **kwargs)
super(ContourWriter, self).__init__(*args, **kwargs)
187 changes: 119 additions & 68 deletions pycoast/cw_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from PIL import Image
import pyproj
import logging
import ast

try:
import configparser
Expand All @@ -32,6 +33,37 @@
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'."""

Expand Down Expand Up @@ -168,7 +200,7 @@ def _add_grid(self, image, area_def,
"""

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]
Expand Down Expand Up @@ -512,7 +544,7 @@ def add_shapes(self, image, area_def, shapes, feature_type=None, x_offset=0, y_o
"""
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]
Expand Down Expand Up @@ -644,22 +676,12 @@ def _iterate_db(self, db_name, tag, resolution, level, zero_pad, db_root_path=No
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'):
Expand All @@ -673,56 +695,79 @@ def add_overlay_from_config(self, config_file, area_def):
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'):
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))

# 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
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:
default_resolution = "c"
elif res > 5000:
default_resolution = "l"
elif res > 1000:
default_resolution = "i"
elif res > 200:
default_resolution = "h"
logger.info("No overlay image found, new overlay image will be saved in cache.")

x_size = area_def.width
y_size = area_def.height
if cache_file is None and background is not None:
foreground = background
else:
default_resolution = "f"
foreground = Image.new('RGBA', (x_size, y_size), (0, 0, 0, 0))

default_resolution = get_resolution_from_area(area_def)

DEFAULT = {'level': 1,
'outline': 'white',
Expand All @@ -734,18 +779,9 @@ def add_overlay_from_config(self, config_file, area_def):
'y_offset': 0,
'resolution': default_resolution}

SECTIONS = ['coasts', 'rivers', 'borders', 'cities', 'grid']
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,
Expand Down Expand Up @@ -800,9 +836,10 @@ def add_overlay_from_config(self, config_file, area_def):
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').lower() in \
['true', 'yes', '1']

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:
Expand Down Expand Up @@ -832,9 +869,23 @@ def add_overlay_from_config(self, config_file, area_def):
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
Expand All @@ -846,7 +897,7 @@ def add_cities(self, image, area_def, citylist, font_file, font_size,
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]
Expand Down
Binary file added pycoast/tests/contours_europe_alpha.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions pycoast/tests/test_data/test_config.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[coasts]
level=4
resolution=l

[borders]
outline=(255, 0, 0)
resolution=c

[rivers]
level=5
outline=blue
resolution=c
Loading

0 comments on commit 00c38b8

Please sign in to comment.