From 500b3203e94c02379959fffdf55d8a3084c102ff Mon Sep 17 00:00:00 2001 From: Martin Raspaud Date: Fri, 11 Oct 2019 14:04:26 +0200 Subject: [PATCH 01/11] Add dict configuration and some tests --- docs/source/config.rst | 7 +- pycoast/__init__.py | 4 +- pycoast/cw_base.py | 160 +++++++++++++++--------- pycoast/tests/contours_europe_alpha.png | Bin 0 -> 33846 bytes pycoast/tests/test_data/test_config.ini | 12 ++ pycoast/tests/test_pycoast.py | 107 ++++++++++++++++ 6 files changed, 223 insertions(+), 67 deletions(-) create mode 100644 pycoast/tests/contours_europe_alpha.png create mode 100644 pycoast/tests/test_data/test_config.ini diff --git a/docs/source/config.rst b/docs/source/config.rst index 79b004f..59ec017 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -1,7 +1,7 @@ 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,13 +9,14 @@ pycoast options you need: [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 diff --git a/pycoast/__init__.py b/pycoast/__init__.py index 6cae0fb..6e1ef1e 100644 --- a/pycoast/__init__.py +++ b/pycoast/__init__.py @@ -3,9 +3,9 @@ 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 class ContourWriter(ContourWriterPIL): @@ -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) \ No newline at end of file + super(ContourWriter, self).__init__(*args, **kwargs) diff --git a/pycoast/cw_base.py b/pycoast/cw_base.py index a666f62..80ed889 100644 --- a/pycoast/cw_base.py +++ b/pycoast/cw_base.py @@ -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,36 @@ logger = logging.getLogger(__name__) +def get_resolution_from_area(area_def): + """Get the best resolution for an area definition.""" + x_size = area_def.x_size + y_size = area_def.y_size + 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: + 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'.""" @@ -619,7 +650,7 @@ def _iterate_db(self, db_name, tag, resolution, level, zero_pad, db_root_path=No if type(level) not in (list,): level = range(1,level+1) - + for i in level: # One shapefile per level @@ -644,22 +675,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'): @@ -673,58 +694,69 @@ 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): + """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. + + + 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. + For cache, to 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 the 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) 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") + logger.info("No overlay image found, 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" - else: - default_resolution = "f" + default_resolution = get_resolution_from_area(area_def) DEFAULT = {'level': 1, 'outline': 'white', @@ -736,19 +768,10 @@ def add_overlay_from_config(self, config_file, area_def): '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 - for section, fun in zip(['coasts', 'rivers', 'borders'], + # Coasts, rivers, borders + for section, fun in zip(['coasts', 'rivers', 'borders', 'grid'], [self.add_coastlines, self.add_rivers, self.add_borders]): @@ -803,6 +826,19 @@ def add_overlay_from_config(self, config_file, area_def): return foreground + 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 + + """ + overlays = self._config_to_dict(config_file) + return self.add_overlay_from_dict(overlays, area_def, os.path.getmtime(config_file)) + 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 diff --git a/pycoast/tests/contours_europe_alpha.png b/pycoast/tests/contours_europe_alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..366f100de6ccc220343a0fc462b9d73ec28ba4bc GIT binary patch literal 33846 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 Y1NnA1iB4=)CjbBd07*qoM6N<$f+{0l1^@s6 literal 0 HcmV?d00001 diff --git a/pycoast/tests/test_data/test_config.ini b/pycoast/tests/test_data/test_config.ini new file mode 100644 index 0000000..addb455 --- /dev/null +++ b/pycoast/tests/test_data/test_config.ini @@ -0,0 +1,12 @@ +[coasts] +level=4 +resolution=l + +[borders] +outline=(255, 0, 0) +resolution=c + +[rivers] +level=5 +outline=blue +resolution=c diff --git a/pycoast/tests/test_pycoast.py b/pycoast/tests/test_pycoast.py index eede757..bf7c670 100644 --- a/pycoast/tests/test_pycoast.py +++ b/pycoast/tests/test_pycoast.py @@ -22,6 +22,7 @@ import numpy as np from PIL import Image, ImageFont +import time def tmp(f): f.tmp = True @@ -643,11 +644,117 @@ def test_coastlines_convert_to_rgba_agg(self): self.assertTrue(image_mode == 'RGBA', 'Conversion to RGBA failed.') + +class FakeAreaDef(): + """A fake area definition object.""" + + def __init__(self, proj4_string, area_extent, x_size, y_size): + self.proj4_string = proj4_string + self.area_extent = area_extent + self.x_size = x_size + self.y_size = y_size + self.area_id = 'fakearea' + + +class TestFromConfig(TestPycoast): + """Test burning overlays from a config file.""" + + def test_foreground(self): + """Test generating a transparent foreground.""" + from pycoast import ContourWriterPIL + euro_img = Image.open(os.path.join(os.path.dirname(__file__), + 'contours_europe_alpha.png')) + euro_data = np.array(euro_img) + + # img = Image.new('RGB', (640, 480)) + proj4_string = \ + '+proj=stere +lon_0=8.00 +lat_0=50.00 +lat_ts=50.00 +ellps=WGS84' + area_extent = (-3363403.31, -2291879.85, 2630596.69, 2203620.1) + area_def = FakeAreaDef(proj4_string, area_extent, 640, 480) + cw = ContourWriterPIL(gshhs_root_dir) + config_file = os.path.join(os.path.dirname(__file__), 'test_data', 'test_config.ini') + img = cw.add_overlay_from_config(config_file, area_def) + + res = np.array(img) + self.assertTrue(fft_metric(euro_data, res), + 'Writing of contours failed') + + overlays = {'coasts': {'level': 4, 'resolution': 'l'}, + 'borders': {'outline': (255, 0, 0), 'resolution': 'c'}, + 'rivers': {'outline': 'blue', 'resolution': 'c', 'level': 5}} + + img = cw.add_overlay_from_dict(overlays, area_def) + res = np.array(img) + self.assertTrue(fft_metric(euro_data, res), + 'Writing of contours failed') + + def test_cache(self): + """Test generating a transparent foreground and cache it.""" + from pycoast import ContourWriterPIL + euro_img = Image.open(os.path.join(os.path.dirname(__file__), + 'contours_europe_alpha.png')) + euro_data = np.array(euro_img) + + # img = Image.new('RGB', (640, 480)) + proj4_string = \ + '+proj=stere +lon_0=8.00 +lat_0=50.00 +lat_ts=50.00 +ellps=WGS84' + area_extent = (-3363403.31, -2291879.85, 2630596.69, 2203620.1) + area_def = FakeAreaDef(proj4_string, area_extent, 640, 480) + cw = ContourWriterPIL(gshhs_root_dir) + + overlays = {'cache': {'file': '/tmp/pycoast_cache'}, + 'coasts': {'level': 4, 'resolution': 'l'}, + 'borders': {'outline': (255, 0, 0), 'resolution': 'c'}, + 'rivers': {'outline': 'blue', 'resolution': 'c', 'level': 5}} + + cache_filename = '/tmp/pycoast_cache_fakearea.png' + img = cw.add_overlay_from_dict(overlays, area_def) + res = np.array(img) + self.assertTrue(fft_metric(euro_data, res), + 'Writing of contours failed') + self.assertTrue(os.path.isfile(cache_filename)) + + current_time = time.time() + + img = cw.add_overlay_from_dict(overlays, area_def, current_time) + + mtime = os.path.getmtime(cache_filename) + + self.assertGreater(mtime, current_time) + self.assertTrue(fft_metric(euro_data, res), + 'Writing of contours failed') + + img = cw.add_overlay_from_dict(overlays, area_def, current_time) + + self.assertEqual(os.path.getmtime(cache_filename), mtime) + self.assertTrue(fft_metric(euro_data, res), + 'Writing of contours failed') + overlays['cache']['regenerate'] = True + img = cw.add_overlay_from_dict(overlays, area_def) + + self.assertNotEqual(os.path.getmtime(cache_filename), mtime) + self.assertTrue(fft_metric(euro_data, res), + 'Writing of contours failed') + os.remove('/tmp/pycoast_cache_fakearea.png') + + def test_get_resolution(self): + """Get the automagical resolution computation.""" + from pycoast import get_resolution_from_area + proj4_string = \ + '+proj=stere +lon_0=8.00 +lat_0=50.00 +lat_ts=50.00 +ellps=WGS84' + area_extent = (-3363403.31, -2291879.85, 2630596.69, 2203620.1) + area_def = FakeAreaDef(proj4_string, area_extent, 640, 480) + self.assertEqual(get_resolution_from_area(area_def), 'l') + area_def = FakeAreaDef(proj4_string, area_extent, 6400, 4800) + self.assertEqual(get_resolution_from_area(area_def), 'h') + + def suite(): loader = unittest.TestLoader() mysuite = unittest.TestSuite() mysuite.addTest(loader.loadTestsFromTestCase(TestPIL)) mysuite.addTest(loader.loadTestsFromTestCase(TestPILAGG)) + mysuite.addTest(loader.loadTestsFromTestCase(TestFromConfig)) return mysuite From 261fa8840e3494ce4e259f8e4e01e55c53f25fad Mon Sep 17 00:00:00 2001 From: Martin Raspaud Date: Mon, 14 Oct 2019 09:27:01 +0200 Subject: [PATCH 02/11] Add a background image argument to the add_overlays_from_config function --- pycoast/cw_base.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/pycoast/cw_base.py b/pycoast/cw_base.py index 80ed889..8db717f 100644 --- a/pycoast/cw_base.py +++ b/pycoast/cw_base.py @@ -63,6 +63,7 @@ def get_resolution_from_area(area_def): else: return "f" + class Proj(pyproj.Proj): """Wrapper around pyproj to add in 'is_latlong'.""" @@ -707,7 +708,7 @@ def _config_to_dict(self, config_file): overlays[section][option] = val return overlays - def add_overlay_from_dict(self, overlays, area_def, cache_epoch=None): + 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: @@ -718,13 +719,18 @@ def add_overlay_from_dict(self, overlays, area_def, cache_epoch=None): 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. + functions in pycoast, so refer to the docstrings of these functions + (add_coastlines, add_rivers, add_borders, add_grid, add_cities). For cache, to 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 @@ -746,6 +752,8 @@ def add_overlay_from_dict(self, overlays, area_def, cache_epoch=None): 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("Regenerating cache file.") @@ -754,7 +762,10 @@ def add_overlay_from_dict(self, overlays, area_def, cache_epoch=None): x_size = area_def.x_size y_size = area_def.y_size - foreground = Image.new('RGBA', (x_size, y_size), (0, 0, 0, 0)) + if cache_file is None and background is not None: + foreground = background + else: + foreground = Image.new('RGBA', (x_size, y_size), (0, 0, 0, 0)) default_resolution = get_resolution_from_area(area_def) @@ -774,7 +785,8 @@ def add_overlay_from_dict(self, overlays, area_def, cache_epoch=None): for section, fun in zip(['coasts', 'rivers', 'borders', 'grid'], [self.add_coastlines, self.add_rivers, - self.add_borders]): + self.add_borders, + self.add_grid]): if section in overlays: @@ -823,10 +835,11 @@ def add_overlay_from_dict(self, overlays, area_def, cache_epoch=None): 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): + 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: @@ -837,7 +850,7 @@ def add_overlay_from_config(self, config_file, area_def): """ overlays = self._config_to_dict(config_file) - return self.add_overlay_from_dict(overlays, area_def, os.path.getmtime(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): From 7ceabb9f2054b0349adb1c1de20adc8ca94563d7 Mon Sep 17 00:00:00 2001 From: Martin Raspaud Date: Mon, 14 Oct 2019 11:01:13 +0200 Subject: [PATCH 03/11] Reorganize imports --- pycoast/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pycoast/__init__.py b/pycoast/__init__.py index 6e1ef1e..e0db1e1 100644 --- a/pycoast/__init__.py +++ b/pycoast/__init__.py @@ -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. From 62a5fc176b19fb5e96ed1b71a1b479193c7ab63f Mon Sep 17 00:00:00 2001 From: Martin Raspaud Date: Tue, 15 Oct 2019 11:58:13 +0200 Subject: [PATCH 04/11] Use @djhoese's branch for CI helpers --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4296a0a..3c1f7a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,8 +26,9 @@ matrix: 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 + - git clone --depth 1 -b all-the-fixes git://github.com/djhoese/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 From 3d1bece0087383ed9875d4805453f9f08dc23aff Mon Sep 17 00:00:00 2001 From: Martin Raspaud Date: Tue, 15 Oct 2019 12:21:57 +0200 Subject: [PATCH 05/11] Fix typo --- pycoast/cw_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycoast/cw_base.py b/pycoast/cw_base.py index 8db717f..dd6533b 100644 --- a/pycoast/cw_base.py +++ b/pycoast/cw_base.py @@ -731,10 +731,10 @@ def add_overlay_from_dict(self, overlays, area_def, cache_epoch=None, background 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, to parameters are configurable: `file` which specifies the directory + 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 the cached file. + True or False (default) to force the overwriting of an already cached file. """ From 35d984dc7ae4388fd2f3c3705575831b9129e7ae Mon Sep 17 00:00:00 2001 From: Martin Raspaud Date: Tue, 15 Oct 2019 18:01:18 +0200 Subject: [PATCH 06/11] Switch back ci-helpers to master --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3c1f7a0..ee0ee34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,8 +26,7 @@ matrix: os: windows language: bash install: -# - git clone --depth 1 git://github.com/astropy/ci-helpers.git - - git clone --depth 1 -b all-the-fixes git://github.com/djhoese/ci-helpers.git + - 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: From 5f135462e9aa5d07700363492875b3edf8a60833 Mon Sep 17 00:00:00 2001 From: Martin Raspaud Date: Tue, 15 Oct 2019 18:16:06 +0200 Subject: [PATCH 07/11] Fix hardcoded /tmp in tests Should make windows happier. --- pycoast/tests/test_pycoast.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pycoast/tests/test_pycoast.py b/pycoast/tests/test_pycoast.py index bf7c670..9705c64 100644 --- a/pycoast/tests/test_pycoast.py +++ b/pycoast/tests/test_pycoast.py @@ -691,6 +691,7 @@ def test_foreground(self): def test_cache(self): """Test generating a transparent foreground and cache it.""" from pycoast import ContourWriterPIL + from tempfile import gettempdir euro_img = Image.open(os.path.join(os.path.dirname(__file__), 'contours_europe_alpha.png')) euro_data = np.array(euro_img) @@ -702,12 +703,14 @@ def test_cache(self): area_def = FakeAreaDef(proj4_string, area_extent, 640, 480) cw = ContourWriterPIL(gshhs_root_dir) - overlays = {'cache': {'file': '/tmp/pycoast_cache'}, + tmp = gettempdir() + + overlays = {'cache': {'file': os.path.join(tmp, 'pycoast_cache')}, 'coasts': {'level': 4, 'resolution': 'l'}, 'borders': {'outline': (255, 0, 0), 'resolution': 'c'}, 'rivers': {'outline': 'blue', 'resolution': 'c', 'level': 5}} - cache_filename = '/tmp/pycoast_cache_fakearea.png' + cache_filename = os.path.join(tmp, 'pycoast_cache_fakearea.png') img = cw.add_overlay_from_dict(overlays, area_def) res = np.array(img) self.assertTrue(fft_metric(euro_data, res), @@ -735,7 +738,7 @@ def test_cache(self): self.assertNotEqual(os.path.getmtime(cache_filename), mtime) self.assertTrue(fft_metric(euro_data, res), 'Writing of contours failed') - os.remove('/tmp/pycoast_cache_fakearea.png') + os.remove(os.path.join(tmp, 'pycoast_cache_fakearea.png')) def test_get_resolution(self): """Get the automagical resolution computation.""" From 96c1aaec341d1efbd69b1198212ff4d58e70c7c0 Mon Sep 17 00:00:00 2001 From: Martin Raspaud Date: Tue, 15 Oct 2019 18:16:28 +0200 Subject: [PATCH 08/11] Change numpy version on travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ee0ee34..f06c5e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ env: global: # Set defaults to avoid repeating in most cases - PYTHON_VERSION=$TRAVIS_PYTHON_VERSION - - NUMPY_VERSION=stable + - NUMPY_VERSION=1.16 - MAIN_CMD='python setup.py' - CONDA_DEPENDENCIES='sphinx pillow pyproj coveralls coverage mock aggdraw six pyshp' - PIP_DEPENDENCIES='' From cbcba5e2f02be453ff79bca0cf7ec81f9e5fe132 Mon Sep 17 00:00:00 2001 From: Martin Raspaud Date: Tue, 15 Oct 2019 18:43:46 +0200 Subject: [PATCH 09/11] Pin numpy version in travis for python 2.7 --- .travis.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index f06c5e1..558d00a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ env: global: # Set defaults to avoid repeating in most cases - PYTHON_VERSION=$TRAVIS_PYTHON_VERSION - - NUMPY_VERSION=1.16 - MAIN_CMD='python setup.py' - CONDA_DEPENDENCIES='sphinx pillow pyproj coveralls coverage mock aggdraw six pyshp' - PIP_DEPENDENCIES='' @@ -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 From 491001bf3059f220d736e8fab27a1af3d0c001a7 Mon Sep 17 00:00:00 2001 From: Martin Raspaud Date: Tue, 15 Oct 2019 18:54:31 +0200 Subject: [PATCH 10/11] Add python 3.7 to the tests --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 558d00a..c090d41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,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 From a268464327638f90cdeb22ad47047c49ac06e20d Mon Sep 17 00:00:00 2001 From: Martin Raspaud Date: Thu, 17 Oct 2019 14:51:48 +0200 Subject: [PATCH 11/11] Fix unittests and deprecation warnings --- pycoast/cw_base.py | 28 ++++++++++++++-------------- pycoast/tests/test_pycoast.py | 6 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pycoast/cw_base.py b/pycoast/cw_base.py index cf143d4..8faba87 100644 --- a/pycoast/cw_base.py +++ b/pycoast/cw_base.py @@ -35,9 +35,9 @@ def get_resolution_from_area(area_def): """Get the best resolution for an area definition.""" - x_size = area_def.x_size - y_size = area_def.y_size - prj = Proj(area_def.proj4_string) + 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]) @@ -200,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] @@ -544,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] @@ -760,8 +760,8 @@ def add_overlay_from_dict(self, overlays, area_def, cache_epoch=None, background except OSError: logger.info("No overlay image found, new overlay image will be saved in cache.") - x_size = area_def.x_size - y_size = area_def.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: @@ -782,11 +782,10 @@ def add_overlay_from_dict(self, overlays, area_def, cache_epoch=None, background is_agg = self._draw_module == "AGG" # Coasts, rivers, borders - for section, fun in zip(['coasts', 'rivers', 'borders', 'grid'], + for section, fun in zip(['coasts', 'rivers', 'borders'], [self.add_coastlines, self.add_rivers, - self.add_borders, - self.add_grid]): + self.add_borders]): if section in overlays: @@ -837,9 +836,10 @@ def add_overlay_from_dict(self, overlays, area_def, cache_epoch=None, background 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: @@ -897,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] diff --git a/pycoast/tests/test_pycoast.py b/pycoast/tests/test_pycoast.py index 94dd966..54ff64b 100644 --- a/pycoast/tests/test_pycoast.py +++ b/pycoast/tests/test_pycoast.py @@ -697,10 +697,10 @@ class FakeAreaDef(): """A fake area definition object.""" def __init__(self, proj4_string, area_extent, x_size, y_size): - self.proj4_string = proj4_string + self.proj_str = proj4_string self.area_extent = area_extent - self.x_size = x_size - self.y_size = y_size + self.width = x_size + self.height = y_size self.area_id = 'fakearea'