pycoast 1.1.0__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pycoast/__init__.py +30 -9
- pycoast/conftest.py +14 -0
- pycoast/cw_agg.py +761 -485
- pycoast/cw_base.py +1575 -785
- pycoast/cw_pil.py +581 -379
- pycoast/tests/__init__.py +1 -0
- pycoast/tests/brazil_shapefiles.png +0 -0
- pycoast/tests/brazil_shapefiles_agg.png +0 -0
- pycoast/tests/coasts_and_grid.ini +13 -0
- pycoast/tests/coasts_and_grid_agg.ini +17 -0
- pycoast/tests/contours_europe.png +0 -0
- pycoast/tests/contours_europe_agg.png +0 -0
- pycoast/tests/contours_europe_alpha.png +0 -0
- pycoast/tests/contours_geos.png +0 -0
- pycoast/tests/contours_geos_agg.png +0 -0
- pycoast/tests/dateline_boundary_cross.png +0 -0
- pycoast/tests/dateline_cross.png +0 -0
- pycoast/tests/eastern_shapes_agg.png +0 -0
- pycoast/tests/eastern_shapes_pil.png +0 -0
- pycoast/tests/grid_europe.png +0 -0
- pycoast/tests/grid_europe_agg.png +0 -0
- pycoast/tests/grid_europe_agg_txt.png +0 -0
- pycoast/tests/grid_from_dict_agg.png +0 -0
- pycoast/tests/grid_from_dict_pil.png +0 -0
- pycoast/tests/grid_geos.png +0 -0
- pycoast/tests/grid_geos_agg.png +0 -0
- pycoast/tests/grid_germ.png +0 -0
- pycoast/tests/grid_nh.png +0 -0
- pycoast/tests/grid_nh_agg.png +0 -0
- pycoast/tests/grid_nh_cfg_agg.png +0 -0
- pycoast/tests/lonlat_boundary_cross.png +0 -0
- pycoast/tests/nh_cities_agg.ini +26 -0
- pycoast/tests/nh_cities_agg.png +0 -0
- pycoast/tests/nh_cities_from_dict_agg.png +0 -0
- pycoast/tests/nh_cities_from_dict_pil.png +0 -0
- pycoast/tests/nh_cities_pil.ini +20 -0
- pycoast/tests/nh_cities_pil.png +0 -0
- pycoast/tests/nh_one_shapefile.ini +11 -0
- pycoast/tests/nh_points_agg.ini +24 -0
- pycoast/tests/nh_points_agg.png +0 -0
- pycoast/tests/nh_points_cfg_pil.png +0 -0
- pycoast/tests/nh_points_pil.ini +19 -0
- pycoast/tests/nh_points_pil.png +0 -0
- pycoast/tests/nh_polygons.png +0 -0
- pycoast/tests/nh_polygons_agg.png +0 -0
- pycoast/tests/no_h_scratch_agg.png +0 -0
- pycoast/tests/no_h_scratch_pil.png +0 -0
- pycoast/tests/no_v_scratch_agg.png +0 -0
- pycoast/tests/no_v_scratch_pil.png +0 -0
- pycoast/tests/one_shapefile_from_cfg_agg.png +0 -0
- pycoast/tests/one_shapefile_from_cfg_pil.png +0 -0
- pycoast/tests/test_data/DejaVuSerif.ttf +0 -0
- pycoast/tests/test_data/gshhs/CITIES/cities.txt +20 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L1.dbf +0 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L1.prj +1 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L1.shp +0 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L1.shx +0 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L2.dbf +0 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L2.prj +1 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L2.shp +0 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L2.shx +0 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L3.dbf +0 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L3.prj +1 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L3.shp +0 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L3.shx +0 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L4.dbf +0 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L4.prj +1 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L4.shp +0 -0
- pycoast/tests/test_data/gshhs/GSHHS_shp/l/GSHHS_l_L4.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_border_c_L1.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_border_c_L1.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_border_c_L1.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_border_c_L1.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_border_c_L2.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_border_c_L2.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_border_c_L2.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_border_c_L2.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_border_c_L3.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_border_c_L3.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_border_c_L3.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_border_c_L3.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L01.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L01.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L01.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L01.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L02.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L02.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L02.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L02.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L03.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L03.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L03.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L03.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L04.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L04.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L04.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L04.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L05.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L05.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L05.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L05.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L06.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L06.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L06.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L06.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L07.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L07.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L07.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L07.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L08.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L08.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L08.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L08.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L09.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L09.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L09.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L09.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L1.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L1.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L1.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L10.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L10.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L10.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L10.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L11.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L11.prj +1 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L11.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L11.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L2.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L2.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L2.shx +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L3.dbf +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L3.shp +0 -0
- pycoast/tests/test_data/gshhs/WDBII_shp/c/WDBII_river_c_L3.shx +0 -0
- pycoast/tests/test_data/shapes/Metareas.dbf +0 -0
- pycoast/tests/test_data/shapes/Metareas.mxd +0 -0
- pycoast/tests/test_data/shapes/Metareas.prj +1 -0
- pycoast/tests/test_data/shapes/Metareas.sbn +0 -0
- pycoast/tests/test_data/shapes/Metareas.sbx +0 -0
- pycoast/tests/test_data/shapes/Metareas.shp +0 -0
- pycoast/tests/test_data/shapes/Metareas.shx +0 -0
- pycoast/tests/test_data/shapes/README +3 -0
- pycoast/tests/test_data/shapes/divisao_politica/BRASIL.dbf +0 -0
- pycoast/tests/test_data/shapes/divisao_politica/BRASIL.shp +0 -0
- pycoast/tests/test_data/shapes/divisao_politica/BRASIL.shx +0 -0
- pycoast/tests/test_data/shapes/divisao_politica/BR_Capitais.dbf +0 -0
- pycoast/tests/test_data/shapes/divisao_politica/BR_Capitais.shp +0 -0
- pycoast/tests/test_data/shapes/divisao_politica/BR_Capitais.shx +0 -0
- pycoast/tests/test_data/shapes/divisao_politica/BR_Contorno.dbf +0 -0
- pycoast/tests/test_data/shapes/divisao_politica/BR_Contorno.shp +0 -0
- pycoast/tests/test_data/shapes/divisao_politica/BR_Contorno.shx +0 -0
- pycoast/tests/test_data/shapes/divisao_politica/BR_Regioes.dbf +0 -0
- pycoast/tests/test_data/shapes/divisao_politica/BR_Regioes.shp +0 -0
- pycoast/tests/test_data/shapes/divisao_politica/BR_Regioes.shx +0 -0
- pycoast/tests/test_data/shapes/divisao_politica/divisao_politica.txt +40 -0
- pycoast/tests/test_data/shapes/divisao_politica/leia.txt +9 -0
- pycoast/tests/test_data/shapes/metarea5.gsf +0 -0
- pycoast/tests/test_data/shapes/metarea5.tbl +21 -0
- pycoast/tests/test_data/shapes/metarea5.tbl.info +25 -0
- pycoast/tests/test_data/test_config.ini +12 -0
- pycoast/tests/test_pycoast.py +1913 -434
- pycoast/tests/two_shapefiles_agg.png +0 -0
- pycoast/tests/two_shapefiles_pil.png +0 -0
- pycoast/tests/western_shapes_agg.png +0 -0
- pycoast/tests/western_shapes_pil.png +0 -0
- pycoast/version.py +19 -17
- pycoast-1.8.0.dist-info/METADATA +107 -0
- pycoast-1.8.0.dist-info/RECORD +171 -0
- {pycoast-1.1.0.dist-info → pycoast-1.8.0.dist-info}/WHEEL +1 -1
- pycoast-1.8.0.dist-info/licenses/LICENSE.txt +201 -0
- pycoast-1.1.0.dist-info/DESCRIPTION.rst +0 -3
- pycoast-1.1.0.dist-info/METADATA +0 -24
- pycoast-1.1.0.dist-info/RECORD +0 -13
- pycoast-1.1.0.dist-info/metadata.json +0 -1
- {pycoast-1.1.0.dist-info → pycoast-1.8.0.dist-info}/top_level.txt +0 -0
pycoast/cw_base.py
CHANGED
|
@@ -1,453 +1,191 @@
|
|
|
1
|
-
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
# pycoast, Writing of coastlines, borders and rivers to images in Python
|
|
4
|
-
#
|
|
5
|
-
# Copyright (C) 2011-2015
|
|
6
|
-
# Esben S. Nielsen
|
|
7
|
-
# Hróbjartur Þorsteinsson
|
|
8
|
-
# Stefano Cerino
|
|
9
|
-
# Katja Hungershofer
|
|
10
|
-
# Panu Lahtinen
|
|
11
|
-
#
|
|
12
|
-
# This program is free software: you can redistribute it and/or modify
|
|
13
|
-
# it under the terms of the GNU General Public License as published by
|
|
14
|
-
# the Free Software Foundation, either version 3 of the License, or
|
|
15
|
-
# (at your option) any later version.
|
|
16
|
-
#
|
|
17
|
-
# This program is distributed in the hope that it will be useful,
|
|
18
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
19
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
20
|
-
# GNU General Public License for more details.
|
|
21
|
-
#
|
|
22
|
-
# You should have received a copy of the GNU General Public License
|
|
23
|
-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
1
|
+
"""Base class for contour writers."""
|
|
24
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import configparser
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import math
|
|
25
11
|
import os
|
|
26
|
-
import
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Generator
|
|
14
|
+
|
|
27
15
|
import numpy as np
|
|
28
|
-
|
|
29
|
-
import
|
|
30
|
-
import
|
|
16
|
+
import shapefile
|
|
17
|
+
from PIL import Image
|
|
18
|
+
from pyproj import CRS, Proj
|
|
31
19
|
|
|
32
20
|
try:
|
|
33
|
-
import
|
|
21
|
+
from pyresample import AreaDefinition
|
|
34
22
|
except ImportError:
|
|
35
|
-
|
|
23
|
+
AreaDefinition = None
|
|
36
24
|
|
|
37
25
|
logger = logging.getLogger(__name__)
|
|
38
26
|
|
|
39
27
|
|
|
40
|
-
|
|
28
|
+
def get_resolution_from_area(area_def):
|
|
29
|
+
"""Get the best resolution for an area definition."""
|
|
30
|
+
x_size = area_def.width
|
|
31
|
+
y_size = area_def.height
|
|
32
|
+
x_resolution = abs(area_def.area_extent[2] - area_def.area_extent[0]) / x_size
|
|
33
|
+
y_resolution = abs(area_def.area_extent[3] - area_def.area_extent[1]) / y_size
|
|
34
|
+
res = min(x_resolution, y_resolution)
|
|
35
|
+
if "degree" in area_def.crs.axis_info[0].unit_name:
|
|
36
|
+
res = _estimate_metered_resolution_from_degrees(area_def.crs, res)
|
|
37
|
+
|
|
38
|
+
if res > 25000:
|
|
39
|
+
return "c"
|
|
40
|
+
elif res > 5000:
|
|
41
|
+
return "l"
|
|
42
|
+
elif res > 1000:
|
|
43
|
+
return "i"
|
|
44
|
+
elif res > 200:
|
|
45
|
+
return "h"
|
|
46
|
+
else:
|
|
47
|
+
return "f"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _estimate_metered_resolution_from_degrees(crs: CRS, resolution_degrees: float) -> float:
|
|
51
|
+
major_radius = crs.datum.ellipsoid.semi_major_metre
|
|
52
|
+
# estimate by taking the arc length using the radius
|
|
53
|
+
return major_radius * math.radians(resolution_degrees)
|
|
54
|
+
|
|
41
55
|
|
|
56
|
+
class _CoordConverter:
|
|
57
|
+
"""Convert coordinates from one space to in-bound image pixel column and row.
|
|
58
|
+
|
|
59
|
+
Convert the coordinate (x,y) in the coordinates
|
|
60
|
+
reference system ('lonlat' or 'image') into an image
|
|
61
|
+
x,y coordinate.
|
|
62
|
+
Uses the area_def methods if coord_ref is 'lonlat'.
|
|
63
|
+
Raises ValueError if pixel coordinates are outside the image bounds
|
|
64
|
+
defined by area_def.width and area_def.height.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, coord_ref: str, area_def: AreaDefinition):
|
|
69
|
+
self._area_def = self._check_area_def(area_def)
|
|
70
|
+
convert_methods = {
|
|
71
|
+
"lonlat": self._lonlat_to_pixels,
|
|
72
|
+
"image": self._image_to_pixels,
|
|
73
|
+
}
|
|
74
|
+
if coord_ref not in convert_methods:
|
|
75
|
+
pretty_coord_refs = [f"'{cr_name}'" for cr_name in sorted(convert_methods.keys())]
|
|
76
|
+
raise ValueError(f"'coord_ref' must be one of {pretty_coord_refs}.")
|
|
77
|
+
self._convert_method = convert_methods[coord_ref]
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _check_area_def(area_def):
|
|
81
|
+
if AreaDefinition is None:
|
|
82
|
+
raise ImportError(
|
|
83
|
+
"Missing required 'pyresample' module, please "
|
|
84
|
+
"install it with 'pip install pyresample' or "
|
|
85
|
+
"'conda install pyresample'."
|
|
86
|
+
)
|
|
87
|
+
if not isinstance(area_def, AreaDefinition):
|
|
88
|
+
raise ValueError("'area_def' must be an instance of AreaDefinition")
|
|
89
|
+
return area_def
|
|
90
|
+
|
|
91
|
+
def __call__(self, x, y):
|
|
92
|
+
return self._convert_method(x, y)
|
|
93
|
+
|
|
94
|
+
def _lonlat_to_pixels(self, x, y):
|
|
95
|
+
return self._area_def.get_array_indices_from_lonlat(x, y)
|
|
96
|
+
|
|
97
|
+
def _image_to_pixels(self, x, y):
|
|
98
|
+
area_def = self._area_def
|
|
99
|
+
x, y = (int(x), int(y))
|
|
100
|
+
if x < 0:
|
|
101
|
+
x += area_def.width
|
|
102
|
+
if y < 0:
|
|
103
|
+
y += area_def.height
|
|
104
|
+
if x < 0 or y < 0 or x >= area_def.width or y >= area_def.height:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"Image pixel coords out of image bounds " f"(width={area_def.width}, height={area_def.height})."
|
|
107
|
+
)
|
|
108
|
+
return x, y
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def hash_dict(dict_to_hash: dict) -> str:
|
|
112
|
+
"""Hash dict object by serializing with json."""
|
|
113
|
+
dhash = hashlib.sha256()
|
|
114
|
+
encoded = json.dumps(dict_to_hash, sort_keys=True).encode()
|
|
115
|
+
dhash.update(encoded)
|
|
116
|
+
return dhash.hexdigest()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ContourWriterBase(object):
|
|
42
120
|
"""Base class for contourwriters. Do not instantiate.
|
|
43
121
|
|
|
44
122
|
:Parameters:
|
|
45
|
-
|
|
46
|
-
|
|
123
|
+
db_root_path : str
|
|
124
|
+
Path to root dir of GSHHS and WDBII shapefiles
|
|
125
|
+
|
|
47
126
|
"""
|
|
48
127
|
|
|
49
|
-
_draw_module =
|
|
128
|
+
_draw_module = "FIXME"
|
|
50
129
|
# This is a flag to make _add_grid aware of which draw.text
|
|
51
130
|
# subroutine, from PIL, aggdraw or cairo is being used
|
|
52
131
|
# (unfortunately they are not fully compatible).
|
|
53
132
|
|
|
54
133
|
def __init__(self, db_root_path=None):
|
|
55
134
|
if db_root_path is None:
|
|
56
|
-
self.db_root_path = os.environ.get(
|
|
135
|
+
self.db_root_path = os.environ.get("GSHHS_DATA_ROOT")
|
|
57
136
|
else:
|
|
58
137
|
self.db_root_path = db_root_path
|
|
59
138
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"""
|
|
63
|
-
|
|
139
|
+
@property
|
|
140
|
+
def is_agg(self) -> bool:
|
|
141
|
+
"""Get if we are using the 'agg' backend."""
|
|
142
|
+
return self._draw_module == "AGG"
|
|
143
|
+
|
|
144
|
+
def _draw_text(self, draw, position, txt, font, align="cc", **kwargs):
|
|
145
|
+
"""Draw text with agg module."""
|
|
146
|
+
if hasattr(draw, "textsize"):
|
|
147
|
+
txt_width, txt_height = draw.textsize(txt, font)
|
|
148
|
+
else:
|
|
149
|
+
left, top, right, bottom = draw.textbbox(position, txt, font)
|
|
150
|
+
# bbox is based on "left ascender" anchor for horizontal text
|
|
151
|
+
# but does not include the ascender to top distance.
|
|
152
|
+
# In order to include that additional distance we take height from
|
|
153
|
+
# anchor (`position`) to the bottom of the text. See:
|
|
154
|
+
# https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html#text-anchors
|
|
155
|
+
txt_width, txt_height = right - left, bottom - position[1]
|
|
64
156
|
x_pos, y_pos = position
|
|
65
157
|
ax, ay = align.lower()
|
|
66
|
-
if ax ==
|
|
158
|
+
if ax == "r":
|
|
67
159
|
x_pos = x_pos - txt_width
|
|
68
|
-
elif ax ==
|
|
160
|
+
elif ax == "c":
|
|
69
161
|
x_pos = x_pos - txt_width / 2
|
|
70
162
|
|
|
71
|
-
if ay ==
|
|
163
|
+
if ay == "b":
|
|
72
164
|
y_pos = y_pos - txt_height
|
|
73
|
-
elif ay ==
|
|
74
|
-
y_pos = y_pos -
|
|
165
|
+
elif ay == "c":
|
|
166
|
+
y_pos = y_pos - txt_height / 2
|
|
75
167
|
|
|
76
168
|
self._engine_text_draw(draw, x_pos, y_pos, txt, font, **kwargs)
|
|
77
169
|
|
|
78
170
|
def _engine_text_draw(self, draw, pos, txt, font, **kwargs):
|
|
79
|
-
raise NotImplementedError(
|
|
80
|
-
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"""
|
|
94
|
-
offset by margins and returns an array of coordintes"""
|
|
95
|
-
x_size, y_size = size
|
|
96
|
-
|
|
97
|
-
def is_in_box(x_y, extents):
|
|
98
|
-
x, y = x_y
|
|
99
|
-
xmin, xmax, ymin, ymax = extents
|
|
100
|
-
if xmin < x < xmax and ymin < y < ymax:
|
|
101
|
-
return True
|
|
102
|
-
else:
|
|
103
|
-
return False
|
|
104
|
-
|
|
105
|
-
def crossing(x1, x2, lim):
|
|
106
|
-
if (x1 < lim) != (x2 < lim):
|
|
107
|
-
return True
|
|
108
|
-
else:
|
|
109
|
-
return False
|
|
110
|
-
|
|
111
|
-
# set box limits
|
|
112
|
-
xlim1 = margins[0]
|
|
113
|
-
ylim1 = margins[1]
|
|
114
|
-
xlim2 = x_size - margins[0]
|
|
115
|
-
ylim2 = y_size - margins[0]
|
|
116
|
-
|
|
117
|
-
# only consider crossing within a box a little bigger than grid
|
|
118
|
-
# boundary
|
|
119
|
-
search_box = (-10, x_size + 10, -10, y_size + 10)
|
|
120
|
-
|
|
121
|
-
# loop trought line steps and detect crossings
|
|
122
|
-
intercepts = []
|
|
123
|
-
align_left = 'LC'
|
|
124
|
-
align_right = 'RC'
|
|
125
|
-
align_top = 'CT'
|
|
126
|
-
align_bottom = 'CB'
|
|
127
|
-
prev_xy = xys[0]
|
|
128
|
-
for i in range(1, len(xys) - 1):
|
|
129
|
-
xy = xys[i]
|
|
130
|
-
if is_in_box(xy, search_box):
|
|
131
|
-
# crossing LHS
|
|
132
|
-
if crossing(prev_xy[0], xy[0], xlim1):
|
|
133
|
-
x = xlim1
|
|
134
|
-
y = xy[1]
|
|
135
|
-
intercepts.append(((x, y), align_left))
|
|
136
|
-
# crossing RHS
|
|
137
|
-
elif crossing(prev_xy[0], xy[0], xlim2):
|
|
138
|
-
x = xlim2
|
|
139
|
-
y = xy[1]
|
|
140
|
-
intercepts.append(((x, y), align_right))
|
|
141
|
-
# crossing Top
|
|
142
|
-
elif crossing(prev_xy[1], xy[1], ylim1):
|
|
143
|
-
x = xy[0]
|
|
144
|
-
y = ylim1
|
|
145
|
-
intercepts.append(((x, y), align_top))
|
|
146
|
-
# crossing Bottom
|
|
147
|
-
elif crossing(prev_xy[1], xy[1], ylim2):
|
|
148
|
-
x = xy[0] # - txt_width/2
|
|
149
|
-
y = ylim2 # - txt_height
|
|
150
|
-
intercepts.append(((x, y), align_bottom))
|
|
151
|
-
prev_xy = xy
|
|
152
|
-
|
|
153
|
-
return intercepts
|
|
154
|
-
|
|
155
|
-
def _add_grid(self, image, area_def,
|
|
156
|
-
Dlon, Dlat,
|
|
157
|
-
dlon, dlat,
|
|
158
|
-
font=None, write_text=True, **kwargs):
|
|
159
|
-
"""Add a lat lon grid to image
|
|
160
|
-
"""
|
|
161
|
-
|
|
162
|
-
try:
|
|
163
|
-
proj4_string = area_def.proj4_string
|
|
164
|
-
area_extent = area_def.area_extent
|
|
165
|
-
except AttributeError:
|
|
166
|
-
proj4_string = area_def[0]
|
|
167
|
-
area_extent = area_def[1]
|
|
168
|
-
|
|
171
|
+
raise NotImplementedError("Text drawing undefined for render engine")
|
|
172
|
+
|
|
173
|
+
def _add_grid(
|
|
174
|
+
self,
|
|
175
|
+
image,
|
|
176
|
+
area_def,
|
|
177
|
+
Dlon,
|
|
178
|
+
Dlat,
|
|
179
|
+
dlon,
|
|
180
|
+
dlat,
|
|
181
|
+
font=None,
|
|
182
|
+
write_text=True,
|
|
183
|
+
**kwargs,
|
|
184
|
+
):
|
|
185
|
+
"""Add a lat lon grid to image."""
|
|
169
186
|
draw = self._get_canvas(image)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
# use kwargs for major lines ... but reform for minor lines:
|
|
174
|
-
minor_line_kwargs = kwargs.copy()
|
|
175
|
-
minor_line_kwargs['outline'] = kwargs['minor_outline']
|
|
176
|
-
if is_agg:
|
|
177
|
-
minor_line_kwargs['outline_opacity'] = \
|
|
178
|
-
kwargs['minor_outline_opacity']
|
|
179
|
-
minor_line_kwargs['width'] = kwargs['minor_width']
|
|
180
|
-
|
|
181
|
-
# set text fonts
|
|
182
|
-
if font is None:
|
|
183
|
-
font = ImageFont.load_default()
|
|
184
|
-
# text margins (at sides of image frame)
|
|
185
|
-
y_text_margin = 4
|
|
186
|
-
x_text_margin = 4
|
|
187
|
-
|
|
188
|
-
# Area and projection info
|
|
189
|
-
x_size, y_size = image.size
|
|
190
|
-
prj = pyproj.Proj(proj4_string)
|
|
191
|
-
|
|
192
|
-
x_offset = 0
|
|
193
|
-
y_offset = 0
|
|
194
|
-
|
|
195
|
-
# Calculate min and max lons and lats of interest
|
|
196
|
-
lon_min, lon_max, lat_min, lat_max = \
|
|
197
|
-
_get_lon_lat_bounding_box(area_extent, x_size, y_size, prj)
|
|
198
|
-
|
|
199
|
-
# Handle dateline crossing
|
|
200
|
-
if lon_max < lon_min:
|
|
201
|
-
lon_max = 360 + lon_max
|
|
202
|
-
|
|
203
|
-
# Draw lonlat grid lines ...
|
|
204
|
-
# create adjustment of line lengths to avoid cluttered pole lines
|
|
205
|
-
if lat_max == 90.0:
|
|
206
|
-
shorten_max_lat = Dlat
|
|
207
|
-
else:
|
|
208
|
-
shorten_max_lat = 0.0
|
|
209
|
-
|
|
210
|
-
if lat_min == -90.0:
|
|
211
|
-
increase_min_lat = Dlat
|
|
212
|
-
else:
|
|
213
|
-
increase_min_lat = 0.0
|
|
214
|
-
|
|
215
|
-
# major lon lines
|
|
216
|
-
round_lon_min = (lon_min - (lon_min % Dlon))
|
|
217
|
-
maj_lons = np.arange(round_lon_min, lon_max, Dlon)
|
|
218
|
-
maj_lons[maj_lons > 180] = maj_lons[maj_lons > 180] - 360
|
|
219
|
-
|
|
220
|
-
# minor lon lines (ticks)
|
|
221
|
-
min_lons = np.arange(round_lon_min, lon_max, dlon)
|
|
222
|
-
min_lons[min_lons > 180] = min_lons[min_lons > 180] - 360
|
|
223
|
-
|
|
224
|
-
# Get min_lons not in maj_lons
|
|
225
|
-
min_lons = np.lib.arraysetops.setdiff1d(min_lons, maj_lons)
|
|
226
|
-
|
|
227
|
-
# lats along major lon lines
|
|
228
|
-
lin_lats = np.arange(lat_min + increase_min_lat,
|
|
229
|
-
lat_max - shorten_max_lat,
|
|
230
|
-
float(lat_max - lat_min) / y_size)
|
|
231
|
-
# lin_lats in rather high definition so that it can be used to
|
|
232
|
-
# posituion text labels near edges of image...
|
|
233
|
-
|
|
234
|
-
# perhaps better to find the actual length of line in pixels...
|
|
235
|
-
|
|
236
|
-
round_lat_min = (lat_min - (lat_min % Dlat))
|
|
237
|
-
|
|
238
|
-
# major lat lines
|
|
239
|
-
maj_lats = np.arange(round_lat_min + increase_min_lat, lat_max, Dlat)
|
|
240
|
-
|
|
241
|
-
# minor lon lines (ticks)
|
|
242
|
-
min_lats = np.arange(round_lat_min + increase_min_lat,
|
|
243
|
-
lat_max - shorten_max_lat,
|
|
244
|
-
dlat)
|
|
245
|
-
|
|
246
|
-
# Get min_lats not in maj_lats
|
|
247
|
-
min_lats = np.lib.arraysetops.setdiff1d(min_lats, maj_lats)
|
|
248
|
-
|
|
249
|
-
# lons along major lat lines (extended slightly to avoid missing the
|
|
250
|
-
# end)
|
|
251
|
-
lin_lons = np.arange(lon_min, lon_max + Dlon / 5.0, Dlon / 10.0)
|
|
252
|
-
|
|
253
|
-
# create dummpy shape object
|
|
254
|
-
tmpshape = shapefile.Writer("")
|
|
255
|
-
|
|
256
|
-
# MINOR LINES ######
|
|
257
|
-
if not kwargs['minor_is_tick']:
|
|
258
|
-
# minor lat lines
|
|
259
|
-
for lat in min_lats:
|
|
260
|
-
lonlats = [(x, lat) for x in lin_lons]
|
|
261
|
-
tmpshape.points = lonlats
|
|
262
|
-
index_arrays, is_reduced = _get_pixel_index(tmpshape,
|
|
263
|
-
area_extent,
|
|
264
|
-
x_size, y_size,
|
|
265
|
-
prj,
|
|
266
|
-
x_offset=x_offset,
|
|
267
|
-
y_offset=y_offset)
|
|
268
|
-
del is_reduced
|
|
269
|
-
# Skip empty datasets
|
|
270
|
-
if len(index_arrays) == 0:
|
|
271
|
-
continue
|
|
272
|
-
# make PIL draw the tick line...
|
|
273
|
-
for index_array in index_arrays:
|
|
274
|
-
self._draw_line(draw,
|
|
275
|
-
index_array.flatten().tolist(),
|
|
276
|
-
**minor_line_kwargs)
|
|
277
|
-
# minor lon lines
|
|
278
|
-
for lon in min_lons:
|
|
279
|
-
lonlats = [(lon, x) for x in lin_lats]
|
|
280
|
-
tmpshape.points = lonlats
|
|
281
|
-
index_arrays, is_reduced = _get_pixel_index(tmpshape,
|
|
282
|
-
area_extent,
|
|
283
|
-
x_size, y_size,
|
|
284
|
-
prj,
|
|
285
|
-
x_offset=x_offset,
|
|
286
|
-
y_offset=y_offset)
|
|
287
|
-
# Skip empty datasets
|
|
288
|
-
if len(index_arrays) == 0:
|
|
289
|
-
continue
|
|
290
|
-
# make PIL draw the tick line...
|
|
291
|
-
for index_array in index_arrays:
|
|
292
|
-
self._draw_line(draw,
|
|
293
|
-
index_array.flatten().tolist(),
|
|
294
|
-
**minor_line_kwargs)
|
|
295
|
-
|
|
296
|
-
# MAJOR LINES AND MINOR TICKS ######
|
|
297
|
-
# major lon lines and tick marks:
|
|
298
|
-
for lon in maj_lons:
|
|
299
|
-
# Draw 'minor' tick lines dlat separation along the lon
|
|
300
|
-
if kwargs['minor_is_tick']:
|
|
301
|
-
tick_lons = np.arange(lon - Dlon / 20.0,
|
|
302
|
-
lon + Dlon / 20.0,
|
|
303
|
-
Dlon / 50.0)
|
|
304
|
-
|
|
305
|
-
for lat in min_lats:
|
|
306
|
-
lonlats = [(x, lat) for x in tick_lons]
|
|
307
|
-
tmpshape.points = lonlats
|
|
308
|
-
index_arrays, is_reduced = \
|
|
309
|
-
_get_pixel_index(tmpshape,
|
|
310
|
-
area_extent,
|
|
311
|
-
x_size, y_size,
|
|
312
|
-
prj,
|
|
313
|
-
x_offset=x_offset,
|
|
314
|
-
y_offset=y_offset)
|
|
315
|
-
# Skip empty datasets
|
|
316
|
-
if len(index_arrays) == 0:
|
|
317
|
-
continue
|
|
318
|
-
# make PIL draw the tick line...
|
|
319
|
-
for index_array in index_arrays:
|
|
320
|
-
self._draw_line(draw,
|
|
321
|
-
index_array.flatten().tolist(),
|
|
322
|
-
**minor_line_kwargs)
|
|
323
|
-
|
|
324
|
-
# Draw 'major' lines
|
|
325
|
-
lonlats = [(lon, x) for x in lin_lats]
|
|
326
|
-
tmpshape.points = lonlats
|
|
327
|
-
index_arrays, is_reduced = _get_pixel_index(tmpshape, area_extent,
|
|
328
|
-
x_size, y_size,
|
|
329
|
-
prj,
|
|
330
|
-
x_offset=x_offset,
|
|
331
|
-
y_offset=y_offset)
|
|
332
|
-
# Skip empty datasets
|
|
333
|
-
if len(index_arrays) == 0:
|
|
334
|
-
continue
|
|
335
|
-
|
|
336
|
-
# make PIL draw the lines...
|
|
337
|
-
for index_array in index_arrays:
|
|
338
|
-
self._draw_line(draw,
|
|
339
|
-
index_array.flatten().tolist(),
|
|
340
|
-
**kwargs)
|
|
341
|
-
|
|
342
|
-
# add lon text markings at each end of longitude line
|
|
343
|
-
if write_text:
|
|
344
|
-
if lon > 0.0:
|
|
345
|
-
txt = "%.2dE" % (lon)
|
|
346
|
-
else:
|
|
347
|
-
txt = "%.2dW" % (-lon)
|
|
348
|
-
xys = self._find_line_intercepts(index_array, image.size,
|
|
349
|
-
(x_text_margin,
|
|
350
|
-
y_text_margin))
|
|
351
|
-
|
|
352
|
-
self._draw_grid_labels(draw, xys, 'lon_placement',
|
|
353
|
-
txt, font, **kwargs)
|
|
354
|
-
|
|
355
|
-
# major lat lines and tick marks:
|
|
356
|
-
for lat in maj_lats:
|
|
357
|
-
# Draw 'minor' tick dlon separation along the lat
|
|
358
|
-
if kwargs['minor_is_tick']:
|
|
359
|
-
tick_lats = np.arange(lat - Dlat / 20.0,
|
|
360
|
-
lat + Dlat / 20.0,
|
|
361
|
-
Dlat / 50.0)
|
|
362
|
-
for lon in min_lons:
|
|
363
|
-
lonlats = [(lon, x) for x in tick_lats]
|
|
364
|
-
tmpshape.points = lonlats
|
|
365
|
-
index_arrays, is_reduced = \
|
|
366
|
-
_get_pixel_index(tmpshape, area_extent,
|
|
367
|
-
x_size, y_size,
|
|
368
|
-
prj,
|
|
369
|
-
x_offset=x_offset,
|
|
370
|
-
y_offset=y_offset)
|
|
371
|
-
# Skip empty datasets
|
|
372
|
-
if len(index_arrays) == 0:
|
|
373
|
-
continue
|
|
374
|
-
# make PIL draw the tick line...
|
|
375
|
-
for index_array in index_arrays:
|
|
376
|
-
self._draw_line(draw,
|
|
377
|
-
index_array.flatten().tolist(),
|
|
378
|
-
**minor_line_kwargs)
|
|
379
|
-
|
|
380
|
-
# Draw 'major' lines
|
|
381
|
-
lonlats = [(x, lat) for x in lin_lons]
|
|
382
|
-
tmpshape.points = lonlats
|
|
383
|
-
index_arrays, is_reduced = _get_pixel_index(tmpshape, area_extent,
|
|
384
|
-
x_size, y_size,
|
|
385
|
-
prj,
|
|
386
|
-
x_offset=x_offset,
|
|
387
|
-
y_offset=y_offset)
|
|
388
|
-
# Skip empty datasets
|
|
389
|
-
if len(index_arrays) == 0:
|
|
390
|
-
continue
|
|
391
|
-
|
|
392
|
-
# make PIL draw the lines...
|
|
393
|
-
for index_array in index_arrays:
|
|
394
|
-
self._draw_line(draw, index_array.flatten().tolist(), **kwargs)
|
|
395
|
-
|
|
396
|
-
# add lat text markings at each end of parallels ...
|
|
397
|
-
if write_text:
|
|
398
|
-
if lat >= 0.0:
|
|
399
|
-
txt = "%.2dN" % (lat)
|
|
400
|
-
else:
|
|
401
|
-
txt = "%.2dS" % (-lat)
|
|
402
|
-
xys = self._find_line_intercepts(index_array, image.size,
|
|
403
|
-
(x_text_margin,
|
|
404
|
-
y_text_margin))
|
|
405
|
-
self._draw_grid_labels(draw, xys, 'lat_placement',
|
|
406
|
-
txt, font, **kwargs)
|
|
407
|
-
|
|
408
|
-
# Draw cross on poles ...
|
|
409
|
-
if lat_max == 90.0:
|
|
410
|
-
crosslats = np.arange(90.0 - Dlat / 2.0, 90.0,
|
|
411
|
-
float(lat_max - lat_min) / y_size)
|
|
412
|
-
for lon in (0.0, 90.0, 180.0, -90.0):
|
|
413
|
-
lonlats = [(lon, x) for x in crosslats]
|
|
414
|
-
tmpshape.points = lonlats
|
|
415
|
-
index_arrays, is_reduced = _get_pixel_index(tmpshape,
|
|
416
|
-
area_extent,
|
|
417
|
-
x_size, y_size,
|
|
418
|
-
prj,
|
|
419
|
-
x_offset=x_offset,
|
|
420
|
-
y_offset=y_offset)
|
|
421
|
-
# Skip empty datasets
|
|
422
|
-
if len(index_arrays) == 0:
|
|
423
|
-
continue
|
|
424
|
-
|
|
425
|
-
# make PIL draw the lines...
|
|
426
|
-
for index_array in index_arrays:
|
|
427
|
-
self._draw_line(draw,
|
|
428
|
-
index_array.flatten().tolist(),
|
|
429
|
-
**kwargs)
|
|
430
|
-
if lat_min == -90.0:
|
|
431
|
-
crosslats = np.arange(-90.0, -90.0 + Dlat / 2.0,
|
|
432
|
-
float(lat_max - lat_min) / y_size)
|
|
433
|
-
for lon in (0.0, 90.0, 180.0, -90.0):
|
|
434
|
-
lonlats = [(lon, x) for x in crosslats]
|
|
435
|
-
tmpshape.points = lonlats
|
|
436
|
-
index_arrays, is_reduced = _get_pixel_index(tmpshape,
|
|
437
|
-
area_extent,
|
|
438
|
-
x_size, y_size,
|
|
439
|
-
prj,
|
|
440
|
-
x_offset=x_offset,
|
|
441
|
-
y_offset=y_offset)
|
|
442
|
-
# Skip empty datasets
|
|
443
|
-
if len(index_arrays) == 0:
|
|
444
|
-
continue
|
|
445
|
-
|
|
446
|
-
# make PIL draw the lines...
|
|
447
|
-
for index_array in index_arrays:
|
|
448
|
-
self._draw_line(draw,
|
|
449
|
-
index_array.flatten().tolist(),
|
|
450
|
-
**kwargs)
|
|
187
|
+
grid_drawer = _GridDrawer(self, draw, area_def, Dlon, Dlat, dlon, dlat, font, write_text, kwargs)
|
|
188
|
+
grid_drawer.draw_grid()
|
|
451
189
|
self._finalize(draw)
|
|
452
190
|
|
|
453
191
|
def _find_bounding_box(self, xys):
|
|
@@ -455,67 +193,96 @@ class ContourWriterBase(object):
|
|
|
455
193
|
lats = [y for (x, y) in xys]
|
|
456
194
|
return [min(lons), min(lats), max(lons), max(lats)]
|
|
457
195
|
|
|
458
|
-
def _add_shapefile_shapes(self, image, area_def, filename,
|
|
459
|
-
|
|
460
|
-
""" for drawing all shapes (polygon/poly-lines) from a custom shape
|
|
461
|
-
file onto a PIL image
|
|
462
|
-
"""
|
|
196
|
+
def _add_shapefile_shapes(self, image, area_def, filename, feature_type=None, **kwargs):
|
|
197
|
+
"""Draw all shapes (polygon/poly-lines) from a shape file onto a PIL Image."""
|
|
463
198
|
sf = shapefile.Reader(filename)
|
|
464
|
-
return self.add_shapes(image, area_def,
|
|
199
|
+
return self.add_shapes(image, area_def, sf.shapes(), feature_type=feature_type, **kwargs)
|
|
200
|
+
|
|
201
|
+
def _add_shapefile_shape(self, image, area_def, filename, shape_id, feature_type=None, **kwargs):
|
|
202
|
+
"""Draw a single shape (polygon/poly-line) definition.
|
|
203
|
+
|
|
204
|
+
Accesses single shape using shape_id from a custom shape file.
|
|
465
205
|
|
|
466
|
-
def _add_shapefile_shape(self, image, area_def, filename, shape_id,
|
|
467
|
-
feature_type=None, **kwargs):
|
|
468
|
-
""" for drawing a single shape (polygon/poly-line) definiton with id,
|
|
469
|
-
shape_id from a custom shape file onto a PIL image
|
|
470
206
|
"""
|
|
471
207
|
sf = shapefile.Reader(filename)
|
|
472
208
|
shape = sf.shape(shape_id)
|
|
473
|
-
return self.add_shapes(image, area_def,
|
|
209
|
+
return self.add_shapes(image, area_def, [shape], feature_type=feature_type, **kwargs)
|
|
474
210
|
|
|
475
211
|
def _add_line(self, image, area_def, lonlats, **kwargs):
|
|
476
|
-
"""
|
|
477
|
-
|
|
212
|
+
"""Draw a custom polyline.
|
|
213
|
+
|
|
214
|
+
Lon and lat coordinates given by the list lonlat.
|
|
215
|
+
|
|
478
216
|
"""
|
|
479
217
|
# create dummpy shapelike object
|
|
480
218
|
shape = type("", (), {})()
|
|
481
219
|
shape.points = lonlats
|
|
482
220
|
shape.parts = [0]
|
|
483
221
|
shape.bbox = self._find_bounding_box(lonlats)
|
|
484
|
-
self.add_shapes(image, area_def, "line",
|
|
222
|
+
self.add_shapes(image, area_def, [shape], feature_type="line", **kwargs)
|
|
485
223
|
|
|
486
224
|
def _add_polygon(self, image, area_def, lonlats, **kwargs):
|
|
487
|
-
"""
|
|
488
|
-
|
|
225
|
+
"""Draw a custom polygon.
|
|
226
|
+
|
|
227
|
+
Lon and lat coordinates given by the list lonlat.
|
|
228
|
+
|
|
489
229
|
"""
|
|
490
230
|
# create dummpy shapelike object
|
|
491
231
|
shape = type("", (), {})()
|
|
492
232
|
shape.points = lonlats
|
|
493
233
|
shape.parts = [0]
|
|
494
234
|
shape.bbox = self._find_bounding_box(lonlats)
|
|
495
|
-
self.add_shapes(image, area_def, "polygon",
|
|
235
|
+
self.add_shapes(image, area_def, [shape], feature_type="polygon", **kwargs)
|
|
236
|
+
|
|
237
|
+
def add_shapes(
|
|
238
|
+
self,
|
|
239
|
+
image,
|
|
240
|
+
area_def,
|
|
241
|
+
shapes,
|
|
242
|
+
feature_type=None,
|
|
243
|
+
x_offset=0,
|
|
244
|
+
y_offset=0,
|
|
245
|
+
**kwargs,
|
|
246
|
+
):
|
|
247
|
+
"""Draw shape objects to PIL image.
|
|
248
|
+
|
|
249
|
+
:Parameters:
|
|
250
|
+
image : Image
|
|
251
|
+
PIL Image to draw shapes on
|
|
252
|
+
area_def : (proj_str, area_extent) or AreaDefinition
|
|
253
|
+
Geolocation information for the provided image
|
|
254
|
+
shapes: iterable
|
|
255
|
+
Series of shape objects from pyshp. Can also be a series
|
|
256
|
+
of 2-element tuples where the first element is the shape
|
|
257
|
+
object and the second is a dictionary of additional drawing
|
|
258
|
+
parameters for this shape.
|
|
259
|
+
feature_type: str
|
|
260
|
+
'polygon' or 'line' or None for what to draw shapes as.
|
|
261
|
+
Default is to draw the shape with the type in the shapefile.
|
|
262
|
+
kwargs:
|
|
263
|
+
Extra drawing keyword arguments for all shapes
|
|
264
|
+
|
|
265
|
+
.. versionchanged: 1.2.0
|
|
266
|
+
|
|
267
|
+
Interface changed to have `shapes` before `feature_type` to allow
|
|
268
|
+
`feature_type` to be optional and default to `None`.
|
|
496
269
|
|
|
497
|
-
def add_shapes(self, image, area_def, feature_type, shapes,
|
|
498
|
-
x_offset=0, y_offset=0, **kwargs):
|
|
499
|
-
""" For drawing shape objects to PIL image - better code reuse of
|
|
500
|
-
drawing shapes - should be used in _add_feature and other methods of
|
|
501
|
-
adding shapes including manually.
|
|
502
270
|
"""
|
|
503
271
|
try:
|
|
504
|
-
|
|
272
|
+
proj_def = area_def.crs if hasattr(area_def, "crs") else area_def.proj_dict
|
|
505
273
|
area_extent = area_def.area_extent
|
|
506
274
|
except AttributeError:
|
|
507
|
-
|
|
275
|
+
proj_def = area_def[0]
|
|
508
276
|
area_extent = area_def[1]
|
|
509
277
|
|
|
510
278
|
draw = self._get_canvas(image)
|
|
511
279
|
|
|
512
280
|
# Area and projection info
|
|
513
281
|
x_size, y_size = image.size
|
|
514
|
-
prj =
|
|
282
|
+
prj = Proj(proj_def)
|
|
515
283
|
|
|
516
284
|
# Calculate min and max lons and lats of interest
|
|
517
|
-
lon_min, lon_max, lat_min, lat_max =
|
|
518
|
-
_get_lon_lat_bounding_box(area_extent, x_size, y_size, prj)
|
|
285
|
+
lon_min, lon_max, lat_min, lat_max = _get_lon_lat_bounding_box(area_extent, x_size, y_size, prj)
|
|
519
286
|
|
|
520
287
|
# Iterate through shapes
|
|
521
288
|
for shape in shapes:
|
|
@@ -527,283 +294,401 @@ class ContourWriterBase(object):
|
|
|
527
294
|
else:
|
|
528
295
|
new_kwargs = kwargs
|
|
529
296
|
|
|
530
|
-
if
|
|
531
|
-
if shape.shapeType == 3:
|
|
532
|
-
ftype = "line"
|
|
533
|
-
elif shape.shapeType == 5:
|
|
534
|
-
ftype = "polygon"
|
|
535
|
-
else:
|
|
536
|
-
raise ValueError("Unsupported shape type: " +
|
|
537
|
-
str(shape.shapeType))
|
|
538
|
-
else:
|
|
539
|
-
ftype = feature_type.lower()
|
|
540
|
-
|
|
541
|
-
# Check if polygon is possibly relevant
|
|
542
|
-
s_lon_ll, s_lat_ll, s_lon_ur, s_lat_ur = shape.bbox
|
|
543
|
-
if lon_min > lon_max:
|
|
544
|
-
pass
|
|
545
|
-
elif (lon_max < s_lon_ll or lon_min > s_lon_ur or
|
|
546
|
-
lat_max < s_lat_ll or lat_min > s_lat_ur):
|
|
547
|
-
# Polygon is irrelevant
|
|
297
|
+
if self._polygon_is_irrelevant(lon_min, lon_max, lat_min, lat_max, shape):
|
|
548
298
|
continue
|
|
549
299
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
x_offset=x_offset,
|
|
563
|
-
y_offset=y_offset)
|
|
564
|
-
|
|
565
|
-
# Skip empty datasets
|
|
566
|
-
if len(index_arrays) == 0:
|
|
567
|
-
continue
|
|
568
|
-
|
|
569
|
-
# Make PIL draw the polygon or line
|
|
570
|
-
for index_array in index_arrays:
|
|
571
|
-
if ftype == 'polygon' and not is_reduced:
|
|
572
|
-
# Draw polygon if dataset has not been reduced
|
|
573
|
-
self._draw_polygon(draw,
|
|
574
|
-
index_array.flatten().tolist(),
|
|
575
|
-
**new_kwargs)
|
|
576
|
-
elif ftype.lower() == 'line' or is_reduced:
|
|
577
|
-
# Draw line
|
|
578
|
-
self._draw_line(draw,
|
|
579
|
-
index_array.flatten().tolist(),
|
|
580
|
-
**new_kwargs)
|
|
581
|
-
else:
|
|
582
|
-
raise ValueError('Unknown contour type: %s' % ftype)
|
|
583
|
-
|
|
300
|
+
self._add_shape(
|
|
301
|
+
shape,
|
|
302
|
+
feature_type,
|
|
303
|
+
area_extent,
|
|
304
|
+
x_size,
|
|
305
|
+
y_size,
|
|
306
|
+
prj,
|
|
307
|
+
x_offset,
|
|
308
|
+
y_offset,
|
|
309
|
+
draw,
|
|
310
|
+
new_kwargs,
|
|
311
|
+
)
|
|
584
312
|
self._finalize(draw)
|
|
585
313
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
314
|
+
@staticmethod
|
|
315
|
+
def _polygon_is_irrelevant(lon_min, lon_max, lat_min, lat_max, shape):
|
|
316
|
+
# Check if polygon is possibly relevant
|
|
317
|
+
s_lon_ll, s_lat_ll, s_lon_ur, s_lat_ur = shape.bbox
|
|
318
|
+
if lon_min <= lon_max:
|
|
319
|
+
# Area_extent west or east of dateline
|
|
320
|
+
shape_is_outside_lon = lon_max < s_lon_ll or lon_min > s_lon_ur
|
|
321
|
+
else:
|
|
322
|
+
# Area_extent spans over dateline
|
|
323
|
+
shape_is_outside_lon = lon_max < s_lon_ll and lon_min > s_lon_ur
|
|
324
|
+
shape_is_outside_lat = lat_max < s_lat_ll or lat_min > s_lat_ur
|
|
325
|
+
return shape_is_outside_lon or shape_is_outside_lat
|
|
326
|
+
|
|
327
|
+
def _add_shape(
|
|
328
|
+
self,
|
|
329
|
+
shape,
|
|
330
|
+
feature_type,
|
|
331
|
+
area_extent,
|
|
332
|
+
x_size,
|
|
333
|
+
y_size,
|
|
334
|
+
prj,
|
|
335
|
+
x_offset,
|
|
336
|
+
y_offset,
|
|
337
|
+
draw,
|
|
338
|
+
new_kwargs,
|
|
339
|
+
):
|
|
340
|
+
ftype = self._feature_type_for_shape(shape, feature_type)
|
|
341
|
+
|
|
342
|
+
# iterate over shape parts (some shapes split into parts)
|
|
343
|
+
# dummy shape part object
|
|
344
|
+
parts = list(shape.parts) + [len(shape.points)]
|
|
345
|
+
for i in range(len(parts) - 1):
|
|
346
|
+
# Get pixel index coordinates of shape
|
|
347
|
+
points = shape.points[parts[i] : parts[i + 1]]
|
|
348
|
+
index_arrays, is_reduced = _get_pixel_index(
|
|
349
|
+
points,
|
|
350
|
+
area_extent,
|
|
351
|
+
x_size,
|
|
352
|
+
y_size,
|
|
353
|
+
prj,
|
|
354
|
+
x_offset=x_offset,
|
|
355
|
+
y_offset=y_offset,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Skip empty datasets
|
|
359
|
+
if len(index_arrays) == 0:
|
|
360
|
+
return
|
|
596
361
|
|
|
597
|
-
|
|
598
|
-
|
|
362
|
+
# Make PIL draw the polygon or line
|
|
363
|
+
for index_array in index_arrays:
|
|
364
|
+
if ftype == "polygon" and not is_reduced:
|
|
365
|
+
# Draw polygon if dataset has not been reduced
|
|
366
|
+
self._draw_polygon(draw, index_array.flatten().tolist(), **new_kwargs)
|
|
367
|
+
elif ftype == "line" or is_reduced:
|
|
368
|
+
# Draw line
|
|
369
|
+
self._draw_line(draw, index_array.flatten().tolist(), **new_kwargs)
|
|
370
|
+
else:
|
|
371
|
+
raise ValueError("Unknown contour type: %s" % ftype)
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def _feature_type_for_shape(shape, feature_type):
|
|
375
|
+
if feature_type is not None:
|
|
376
|
+
return feature_type.lower()
|
|
377
|
+
if shape.shapeType == shapefile.POLYLINE:
|
|
378
|
+
ftype = "line"
|
|
379
|
+
elif shape.shapeType == shapefile.POLYGON:
|
|
380
|
+
ftype = "polygon"
|
|
381
|
+
else:
|
|
382
|
+
raise ValueError("Unsupported shape type: " + str(shape.shapeType))
|
|
383
|
+
return ftype
|
|
384
|
+
|
|
385
|
+
def _add_feature(
|
|
386
|
+
self,
|
|
387
|
+
image,
|
|
388
|
+
area_def,
|
|
389
|
+
feature_type,
|
|
390
|
+
db_name,
|
|
391
|
+
tag=None,
|
|
392
|
+
zero_pad=False,
|
|
393
|
+
resolution="c",
|
|
394
|
+
level=1,
|
|
395
|
+
x_offset=0,
|
|
396
|
+
y_offset=0,
|
|
397
|
+
db_root_path=None,
|
|
398
|
+
**kwargs,
|
|
399
|
+
):
|
|
400
|
+
"""Add a contour feature to image."""
|
|
401
|
+
shape_generator = self._iterate_db(db_name, tag, resolution, level, zero_pad, db_root_path=db_root_path)
|
|
402
|
+
|
|
403
|
+
return self.add_shapes(
|
|
404
|
+
image,
|
|
405
|
+
area_def,
|
|
406
|
+
shape_generator,
|
|
407
|
+
feature_type=feature_type,
|
|
408
|
+
x_offset=x_offset,
|
|
409
|
+
y_offset=y_offset,
|
|
410
|
+
**kwargs,
|
|
411
|
+
)
|
|
599
412
|
|
|
600
413
|
def _iterate_db(self, db_name, tag, resolution, level, zero_pad, db_root_path=None):
|
|
601
|
-
"""Iterate through datasets
|
|
602
|
-
"""
|
|
414
|
+
"""Iterate through datasets."""
|
|
603
415
|
if db_root_path is None:
|
|
604
416
|
db_root_path = self.db_root_path
|
|
605
417
|
if db_root_path is None:
|
|
606
418
|
raise ValueError("'db_root_path' must be specified to use this method")
|
|
607
|
-
|
|
608
|
-
format_string =
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
format_string += 'L%02i.shp'
|
|
614
|
-
else:
|
|
615
|
-
format_string += 'L%s.shp'
|
|
616
|
-
|
|
617
|
-
if type(level) not in (list,):
|
|
618
|
-
level = range(1,level+1)
|
|
619
|
-
|
|
620
|
-
for i in level:
|
|
621
|
-
|
|
622
|
-
# One shapefile per level
|
|
623
|
-
if tag is None:
|
|
624
|
-
shapefilename = \
|
|
625
|
-
os.path.join(db_root_path, '%s_shp' % db_name,
|
|
626
|
-
resolution, format_string %
|
|
627
|
-
(db_name, resolution, i))
|
|
628
|
-
else:
|
|
629
|
-
shapefilename = \
|
|
630
|
-
os.path.join(db_root_path, '%s_shp' % db_name,
|
|
631
|
-
resolution, format_string %
|
|
632
|
-
(db_name, tag, resolution, i))
|
|
419
|
+
levels = range(1, level + 1) if not isinstance(level, list) else level
|
|
420
|
+
format_string, format_params = self._get_db_shapefile_format_and_params(db_name, resolution, tag, zero_pad)
|
|
421
|
+
shapefile_root_dir = os.path.join(db_root_path, f"{db_name}_shp", resolution)
|
|
422
|
+
for i in levels:
|
|
423
|
+
level_format_params = format_params + (i,)
|
|
424
|
+
shapefilename = os.path.join(shapefile_root_dir, format_string % level_format_params)
|
|
633
425
|
try:
|
|
634
426
|
s = shapefile.Reader(shapefilename)
|
|
635
427
|
shapes = s.shapes()
|
|
636
428
|
except AttributeError:
|
|
637
|
-
raise ValueError(
|
|
638
|
-
% shapefilename)
|
|
639
|
-
|
|
640
|
-
for shape in shapes:
|
|
641
|
-
yield shape
|
|
429
|
+
raise ValueError("Could not find shapefile %s" % shapefilename)
|
|
642
430
|
|
|
643
|
-
|
|
644
|
-
"""Do any need finalization of the drawing
|
|
645
|
-
"""
|
|
431
|
+
yield from shapes
|
|
646
432
|
|
|
647
|
-
|
|
433
|
+
@staticmethod
|
|
434
|
+
def _get_db_shapefile_format_and_params(db_name, resolution, tag, zero_pad):
|
|
435
|
+
format_string = "%s_%s_"
|
|
436
|
+
format_params = (db_name, resolution)
|
|
437
|
+
if tag is not None:
|
|
438
|
+
format_string += "%s_"
|
|
439
|
+
format_params = (db_name, tag, resolution)
|
|
648
440
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
441
|
+
if zero_pad:
|
|
442
|
+
format_string += "L%02i.shp"
|
|
443
|
+
else:
|
|
444
|
+
format_string += "L%s.shp"
|
|
445
|
+
return format_string, format_params
|
|
652
446
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
area_def : object
|
|
657
|
-
Area Definition of the creating image
|
|
658
|
-
"""
|
|
447
|
+
def _finalize(self, draw):
|
|
448
|
+
"""Do any need finalization of the drawing."""
|
|
449
|
+
pass
|
|
659
450
|
|
|
451
|
+
def _config_to_dict(self, config_file):
|
|
452
|
+
"""Convert a config file to a dict."""
|
|
660
453
|
config = configparser.ConfigParser()
|
|
661
454
|
try:
|
|
662
|
-
with open(config_file,
|
|
455
|
+
with open(config_file, "r"):
|
|
663
456
|
logger.info("Overlays config file %s found", str(config_file))
|
|
664
457
|
config.read(config_file)
|
|
665
458
|
except IOError:
|
|
666
|
-
logger.error("Overlays config file %s does not exist!",
|
|
667
|
-
str(config_file))
|
|
459
|
+
logger.error("Overlays config file %s does not exist!", str(config_file))
|
|
668
460
|
raise
|
|
669
461
|
except configparser.NoSectionError:
|
|
670
462
|
logger.error("Error in %s", str(config_file))
|
|
671
463
|
raise
|
|
672
464
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
cacheTime = os.path.getmtime(cache_file)
|
|
684
|
-
# Cache file will be used only if it's newer than config file
|
|
685
|
-
if configTime < cacheTime:
|
|
686
|
-
foreground = Image.open(cache_file)
|
|
687
|
-
logger.info('Using image in cache %s', cache_file)
|
|
688
|
-
return foreground
|
|
689
|
-
else:
|
|
690
|
-
logger.info("Cache file is not used "
|
|
691
|
-
"because config file has changed")
|
|
692
|
-
except OSError:
|
|
693
|
-
logger.info("New overlay image will be saved in cache")
|
|
694
|
-
|
|
695
|
-
x_size = area_def.x_size
|
|
696
|
-
y_size = area_def.y_size
|
|
697
|
-
foreground = Image.new('RGBA', (x_size, y_size), (0, 0, 0, 0))
|
|
698
|
-
|
|
699
|
-
# Lines (coasts, rivers, borders) management
|
|
700
|
-
prj = pyproj.Proj(area_def.proj4_string)
|
|
701
|
-
if prj.is_latlong():
|
|
702
|
-
x_ll, y_ll = prj(area_def.area_extent[0], area_def.area_extent[1])
|
|
703
|
-
x_ur, y_ur = prj(area_def.area_extent[2], area_def.area_extent[3])
|
|
704
|
-
x_resolution = (x_ur - x_ll) / x_size
|
|
705
|
-
y_resolution = (y_ur - y_ll) / y_size
|
|
706
|
-
else:
|
|
707
|
-
x_resolution = ((area_def.area_extent[2] -
|
|
708
|
-
area_def.area_extent[0]) /
|
|
709
|
-
x_size)
|
|
710
|
-
y_resolution = ((area_def.area_extent[3] -
|
|
711
|
-
area_def.area_extent[1]) /
|
|
712
|
-
y_size)
|
|
713
|
-
res = min(x_resolution, y_resolution)
|
|
714
|
-
|
|
715
|
-
if res > 25000:
|
|
716
|
-
default_resolution = "c"
|
|
717
|
-
elif res > 5000:
|
|
718
|
-
default_resolution = "l"
|
|
719
|
-
elif res > 1000:
|
|
720
|
-
default_resolution = "i"
|
|
721
|
-
elif res > 200:
|
|
722
|
-
default_resolution = "h"
|
|
723
|
-
else:
|
|
724
|
-
default_resolution = "f"
|
|
725
|
-
|
|
726
|
-
DEFAULT = {'level': 1,
|
|
727
|
-
'outline': 'white',
|
|
728
|
-
'width': 1,
|
|
729
|
-
'fill': None,
|
|
730
|
-
'fill_opacity': 255,
|
|
731
|
-
'outline_opacity': 255,
|
|
732
|
-
'x_offset': 0,
|
|
733
|
-
'y_offset': 0,
|
|
734
|
-
'resolution': default_resolution}
|
|
735
|
-
|
|
736
|
-
SECTIONS = ['coasts', 'rivers', 'borders', 'cities']
|
|
465
|
+
SECTIONS = [
|
|
466
|
+
"cache",
|
|
467
|
+
"coasts",
|
|
468
|
+
"rivers",
|
|
469
|
+
"borders",
|
|
470
|
+
"shapefiles",
|
|
471
|
+
"grid",
|
|
472
|
+
"cities",
|
|
473
|
+
"points",
|
|
474
|
+
]
|
|
737
475
|
overlays = {}
|
|
738
|
-
|
|
739
476
|
for section in config.sections():
|
|
740
477
|
if section in SECTIONS:
|
|
741
478
|
overlays[section] = {}
|
|
742
479
|
for option in config.options(section):
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
480
|
+
val = config.get(section, option)
|
|
481
|
+
try:
|
|
482
|
+
overlays[section][option] = ast.literal_eval(val)
|
|
483
|
+
except ValueError:
|
|
484
|
+
overlays[section][option] = val
|
|
485
|
+
return overlays
|
|
486
|
+
|
|
487
|
+
def add_overlay_from_dict(self, overlays, area_def, cache_epoch=None, background=None):
|
|
488
|
+
"""Create and return a transparent image adding all the overlays contained in the ``overlays`` dict.
|
|
489
|
+
|
|
490
|
+
Optionally caches overlay results for faster rendering of images with
|
|
491
|
+
the same provided AreaDefinition and parameters. Cached results are
|
|
492
|
+
identified by hashing the AreaDefinition and the ``overlays`` dictionary.
|
|
493
|
+
|
|
494
|
+
Note that if ``background`` is provided and caching is not used, the
|
|
495
|
+
result will be the final result of applying the overlays onto the
|
|
496
|
+
background. This is due to an optimization step avoiding creating a
|
|
497
|
+
separate overlay image in memory when it isn't needed.
|
|
498
|
+
|
|
499
|
+
.. warning::
|
|
500
|
+
|
|
501
|
+
Font objects are ignored in parameter hashing as they can't be easily hashed.
|
|
502
|
+
Therefore, font changes will not trigger a new rendering for the cache.
|
|
503
|
+
|
|
504
|
+
:Parameters:
|
|
505
|
+
overlays : dict
|
|
506
|
+
overlays configuration
|
|
507
|
+
area_def : object
|
|
508
|
+
Area Definition of the creating image
|
|
509
|
+
cache_epoch: seconds since epoch
|
|
510
|
+
The latest time allowed for cache the cache file. If the cache
|
|
511
|
+
file is older than this (mtime), the cache should be
|
|
512
|
+
regenerated. Defaults to 0 meaning always reuse the cached
|
|
513
|
+
file if it exists. Requires "cache" to be configured in the
|
|
514
|
+
provided dictionary (see below).
|
|
515
|
+
background: pillow image instance
|
|
516
|
+
The image on which to write the overlays on. If it's None (default),
|
|
517
|
+
a new image is created, otherwise the provided background is
|
|
518
|
+
used and changed *in place*.
|
|
519
|
+
|
|
520
|
+
The keys in ``overlays`` that will be taken into account are:
|
|
521
|
+
cache, coasts, rivers, borders, shapefiles, grid, cities, points
|
|
522
|
+
|
|
523
|
+
For all of them except ``cache``, the items are the same as the
|
|
524
|
+
corresponding functions in pycoast, so refer to the docstrings of
|
|
525
|
+
these functions (add_coastlines, add_rivers, add_borders,
|
|
526
|
+
add_shapefile_shapes, add_grid, add_cities, add_points).
|
|
527
|
+
For cache, two parameters are configurable:
|
|
528
|
+
|
|
529
|
+
- `file`:
|
|
530
|
+
specify the directory and the prefix
|
|
531
|
+
of the file to save the caches decoration to (for example
|
|
532
|
+
/var/run/black_coasts_red_borders)
|
|
533
|
+
- `regenerate`:
|
|
534
|
+
True or False (default) to force the overwriting
|
|
535
|
+
of an already cached file.
|
|
536
|
+
|
|
537
|
+
:Returns: PIL.Image.Image
|
|
538
|
+
|
|
539
|
+
Resulting overlays as an Image object. If caching was used then
|
|
540
|
+
the Image wraps an open file and should be closed by the caller.
|
|
541
|
+
If caching was not used or the cached image was recreated then
|
|
542
|
+
this is an in-memory Image object. Regardless, it can be closed
|
|
543
|
+
by calling the ``.close()`` method of the Image.
|
|
544
|
+
|
|
545
|
+
"""
|
|
546
|
+
overlay_helper = _OverlaysFromDict(self, overlays, area_def, cache_epoch, background)
|
|
547
|
+
return overlay_helper.apply_overlays()
|
|
548
|
+
|
|
549
|
+
def add_overlay_from_config(self, config_file, area_def, background=None):
|
|
550
|
+
"""Create and return a transparent image adding all the overlays contained in a configuration file.
|
|
551
|
+
|
|
552
|
+
See :meth:`add_overlay_from_dict` for more information.
|
|
800
553
|
|
|
801
|
-
|
|
554
|
+
:Parameters:
|
|
555
|
+
config_file : str
|
|
556
|
+
Configuration file name
|
|
557
|
+
area_def : object
|
|
558
|
+
Area Definition of the creating image
|
|
559
|
+
|
|
560
|
+
:Returns: PIL.Image.Image
|
|
561
|
+
|
|
562
|
+
Resulting overlays as an Image object. If caching was used then
|
|
563
|
+
the Image wraps an open file and should be closed by the caller.
|
|
564
|
+
If caching was not used or the cached image was recreated then
|
|
565
|
+
this is an in-memory Image object. Regardless, it can be closed
|
|
566
|
+
by calling the ``.close()`` method of the Image.
|
|
802
567
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
568
|
+
"""
|
|
569
|
+
overlays = self._config_to_dict(config_file)
|
|
570
|
+
return self.add_overlay_from_dict(overlays, area_def, os.path.getmtime(config_file), background)
|
|
571
|
+
|
|
572
|
+
def draw_star(self, draw, symbol, x, y, ptsize, **kwargs):
|
|
573
|
+
# 5 <= n <= 8, symbol = string in ['star8', 'star7', 'star6', 'star5']
|
|
574
|
+
n = int(symbol[4])
|
|
575
|
+
alpha2 = math.pi / n
|
|
576
|
+
# r1 = outer radius (defaults to 0.5 * ptsize), r1 > r2 = inner radius
|
|
577
|
+
r1 = 0.5 * ptsize
|
|
578
|
+
r2 = r1 / (math.cos(alpha2) + math.sin(alpha2) * math.tan(2.0 * alpha2))
|
|
579
|
+
xy = []
|
|
580
|
+
alpha = 0.0
|
|
581
|
+
# Walk from star top ray CW around the symbol
|
|
582
|
+
for i in range(2 * n):
|
|
583
|
+
if (i % 2) == 0:
|
|
584
|
+
xy.append(x + r1 * math.sin(alpha))
|
|
585
|
+
xy.append(y - r1 * math.cos(alpha))
|
|
586
|
+
else:
|
|
587
|
+
xy.append(x + r2 * math.sin(alpha))
|
|
588
|
+
xy.append(y - r2 * math.cos(alpha))
|
|
589
|
+
alpha += alpha2
|
|
590
|
+
self._draw_polygon(draw, xy, **kwargs)
|
|
591
|
+
|
|
592
|
+
def draw_hexagon(self, draw, x, y, ptsize, **kwargs):
|
|
593
|
+
xy = [
|
|
594
|
+
x + 0.25 * ptsize,
|
|
595
|
+
y - 0.4330127 * ptsize,
|
|
596
|
+
x + 0.50 * ptsize,
|
|
597
|
+
y,
|
|
598
|
+
x + 0.25 * ptsize,
|
|
599
|
+
y + 0.4330127 * ptsize,
|
|
600
|
+
x - 0.25 * ptsize,
|
|
601
|
+
y + 0.4330127 * ptsize,
|
|
602
|
+
x - 0.50 * ptsize,
|
|
603
|
+
y,
|
|
604
|
+
x - 0.25 * ptsize,
|
|
605
|
+
y - 0.4330127 * ptsize,
|
|
606
|
+
]
|
|
607
|
+
self._draw_polygon(draw, xy, **kwargs)
|
|
608
|
+
|
|
609
|
+
def draw_pentagon(self, draw, x, y, ptsize, **kwargs):
|
|
610
|
+
xy = [
|
|
611
|
+
x,
|
|
612
|
+
y - 0.5 * ptsize,
|
|
613
|
+
x + 0.4755283 * ptsize,
|
|
614
|
+
y - 0.1545085 * ptsize,
|
|
615
|
+
x + 0.2938926 * ptsize,
|
|
616
|
+
y + 0.4045085 * ptsize,
|
|
617
|
+
x - 0.2938926 * ptsize,
|
|
618
|
+
y + 0.4045085 * ptsize,
|
|
619
|
+
x - 0.4755283 * ptsize,
|
|
620
|
+
y - 0.1545085 * ptsize,
|
|
621
|
+
]
|
|
622
|
+
self._draw_polygon(draw, xy, **kwargs)
|
|
623
|
+
|
|
624
|
+
def draw_triangle(self, draw, x, y, ptsize, **kwargs):
|
|
625
|
+
xy = [
|
|
626
|
+
x,
|
|
627
|
+
y - 0.5 * ptsize,
|
|
628
|
+
x + 0.4330127 * ptsize,
|
|
629
|
+
y + 0.25 * ptsize,
|
|
630
|
+
x - 0.4330127 * ptsize,
|
|
631
|
+
y + 0.25 * ptsize,
|
|
632
|
+
]
|
|
633
|
+
self._draw_polygon(draw, xy, **kwargs)
|
|
634
|
+
|
|
635
|
+
def add_cities(
|
|
636
|
+
self,
|
|
637
|
+
image,
|
|
638
|
+
area_def,
|
|
639
|
+
cities_list,
|
|
640
|
+
font_file,
|
|
641
|
+
font_size=12,
|
|
642
|
+
symbol="circle",
|
|
643
|
+
ptsize=6,
|
|
644
|
+
outline="black",
|
|
645
|
+
fill="white",
|
|
646
|
+
db_root_path=None,
|
|
647
|
+
**kwargs,
|
|
648
|
+
):
|
|
649
|
+
"""Add cities (symbol and UTF-8 names as description) to a PIL image object.
|
|
806
650
|
|
|
651
|
+
:Parameters:
|
|
652
|
+
image : object
|
|
653
|
+
PIL image object
|
|
654
|
+
area_def : object
|
|
655
|
+
Area Definition of the provided image
|
|
656
|
+
cities_list : list of city names ['City1', 'City2', City3, ..., 'CityN']
|
|
657
|
+
| a list of UTF-8 or ASCII strings. If either of these strings is found
|
|
658
|
+
| in file db_root_path/CITIES/cities.red, longitude and latitude is read
|
|
659
|
+
| and the city is added like a point with its UTF-8 name as description
|
|
660
|
+
| e.g. cities_list = ['Zurich', 'Oslo'] will add cities 'Zürich', 'Oslo'.
|
|
661
|
+
| Check the README_PyCoast.txt in archive cities2022.zip for more details.
|
|
662
|
+
font_file : str
|
|
663
|
+
Path to font file
|
|
664
|
+
font_size : int
|
|
665
|
+
Size of font
|
|
666
|
+
symbol : string
|
|
667
|
+
type of symbol, one of the elelments from the list
|
|
668
|
+
['circle', 'hexagon', 'pentagon', 'square', 'triangle',
|
|
669
|
+
'star8', 'star7', 'star6', 'star5', 'asterisk']
|
|
670
|
+
ptsize : int
|
|
671
|
+
Size of the point.
|
|
672
|
+
outline : str or (R, G, B), optional
|
|
673
|
+
Line color of the symbol
|
|
674
|
+
fill : str or (R, G, B), optional
|
|
675
|
+
Filling color of the symbol
|
|
676
|
+
|
|
677
|
+
:Optional keyword arguments:
|
|
678
|
+
width : float
|
|
679
|
+
Line width of the symbol
|
|
680
|
+
outline_opacity : int, optional {0; 255}
|
|
681
|
+
Opacity of the line of the symbol.
|
|
682
|
+
fill_opacity : int, optional {0; 255}
|
|
683
|
+
Opacity of the filling of the symbol
|
|
684
|
+
box_outline : str or (R, G, B), optional
|
|
685
|
+
Line color of the textbox borders.
|
|
686
|
+
box_linewidth : float
|
|
687
|
+
Line width of the the borders of the textbox
|
|
688
|
+
box_fill : str or (R, G, B), optional
|
|
689
|
+
Filling color of the background of the textbox.
|
|
690
|
+
box_opacity : int, optional {0; 255}
|
|
691
|
+
Opacity of the background filling of the textbox.
|
|
807
692
|
"""
|
|
808
693
|
if db_root_path is None:
|
|
809
694
|
db_root_path = self.db_root_path
|
|
@@ -811,128 +696,339 @@ class ContourWriterBase(object):
|
|
|
811
696
|
raise ValueError("'db_root_path' must be specified to use this method")
|
|
812
697
|
|
|
813
698
|
try:
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
699
|
+
from pyresample.geometry import AreaDefinition
|
|
700
|
+
except ImportError:
|
|
701
|
+
raise ImportError("Missing required 'pyresample' module, please install it.")
|
|
702
|
+
|
|
703
|
+
if not isinstance(area_def, AreaDefinition):
|
|
704
|
+
raise ValueError("Expected 'area_def' is an instance of AreaDefinition object")
|
|
819
705
|
|
|
820
706
|
draw = self._get_canvas(image)
|
|
821
707
|
|
|
822
|
-
#
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
708
|
+
# cities.red is a reduced version of the files avalable at http://download.geonames.org
|
|
709
|
+
# Fields: 0=name (UTF-8), 1=asciiname, 2=longitude [°E], 3=latitude [°N], 4=countrycode
|
|
710
|
+
cities_filename = os.path.join(db_root_path, os.path.join("CITIES", "cities.txt"))
|
|
711
|
+
for city_name, lon, lat in iter_cities_names_lon_lat(cities_filename, cities_list):
|
|
712
|
+
try:
|
|
713
|
+
x, y = area_def.get_array_indices_from_lonlat(lon, lat)
|
|
714
|
+
except ValueError:
|
|
715
|
+
logger.info(
|
|
716
|
+
"City %s is out of the area, it will not be added to the image.",
|
|
717
|
+
city_name + " " + str((lon, lat)),
|
|
718
|
+
)
|
|
719
|
+
continue
|
|
720
|
+
# add symbol
|
|
721
|
+
if ptsize != 0:
|
|
722
|
+
half_ptsize = int(round(ptsize / 2.0))
|
|
723
|
+
dot_box = [
|
|
724
|
+
x - half_ptsize,
|
|
725
|
+
y - half_ptsize,
|
|
726
|
+
x + half_ptsize,
|
|
727
|
+
y + half_ptsize,
|
|
728
|
+
]
|
|
729
|
+
|
|
730
|
+
width = kwargs.get("width", 1.0)
|
|
731
|
+
outline_opacity = kwargs.get("outline_opacity", 255)
|
|
732
|
+
fill_opacity = kwargs.get("fill_opacity", 255)
|
|
733
|
+
self._draw_point_element(
|
|
734
|
+
draw,
|
|
735
|
+
symbol,
|
|
736
|
+
dot_box,
|
|
737
|
+
x,
|
|
738
|
+
y,
|
|
739
|
+
width,
|
|
740
|
+
ptsize,
|
|
741
|
+
outline,
|
|
742
|
+
outline_opacity,
|
|
743
|
+
fill,
|
|
744
|
+
fill_opacity,
|
|
745
|
+
)
|
|
746
|
+
text_position = [x + ptsize, y]
|
|
747
|
+
else:
|
|
748
|
+
text_position = [x, y]
|
|
749
|
+
|
|
750
|
+
font = self._get_font(outline, font_file, font_size)
|
|
751
|
+
new_kwargs = kwargs.copy()
|
|
752
|
+
box_outline = new_kwargs.pop("box_outline", "white")
|
|
753
|
+
box_opacity = new_kwargs.pop("box_opacity", 0)
|
|
754
|
+
|
|
755
|
+
# add text_box
|
|
756
|
+
self._draw_text_box(
|
|
757
|
+
draw,
|
|
758
|
+
text_position,
|
|
759
|
+
city_name,
|
|
760
|
+
font,
|
|
761
|
+
outline,
|
|
762
|
+
box_outline,
|
|
763
|
+
box_opacity,
|
|
764
|
+
**new_kwargs,
|
|
765
|
+
)
|
|
766
|
+
logger.info("%s added", city_name + " " + str((lon, lat)))
|
|
767
|
+
self._finalize(draw)
|
|
838
768
|
|
|
839
|
-
|
|
769
|
+
def add_points(
|
|
770
|
+
self,
|
|
771
|
+
image,
|
|
772
|
+
area_def,
|
|
773
|
+
points_list,
|
|
774
|
+
font_file,
|
|
775
|
+
font_size=12,
|
|
776
|
+
symbol="circle",
|
|
777
|
+
ptsize=6,
|
|
778
|
+
outline="black",
|
|
779
|
+
fill="white",
|
|
780
|
+
coord_ref="lonlat",
|
|
781
|
+
**kwargs,
|
|
782
|
+
):
|
|
783
|
+
"""Add a symbol and/or text at the point(s) of interest to a PIL image object.
|
|
840
784
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
785
|
+
:Parameters:
|
|
786
|
+
image : object
|
|
787
|
+
PIL image object
|
|
788
|
+
area_def : object
|
|
789
|
+
Area Definition of the provided image
|
|
790
|
+
points_list : list [((x, y), desc)]
|
|
791
|
+
| a list of points defined with (x, y) in float and a desc in string
|
|
792
|
+
| [((x1, y1), desc1), ((x2, y2), desc2)]
|
|
793
|
+
| See coord_ref (below) for the meaning of x, y.
|
|
794
|
+
| x : float
|
|
795
|
+
| longitude or pixel x of a point
|
|
796
|
+
| y : float
|
|
797
|
+
| latitude or pixel y of a point
|
|
798
|
+
| desc : str
|
|
799
|
+
| description of a point
|
|
800
|
+
font_file : str
|
|
801
|
+
Path to font file
|
|
802
|
+
font_size : int
|
|
803
|
+
Size of font
|
|
804
|
+
symbol : string
|
|
805
|
+
type of symbol, one of the elelments from the list
|
|
806
|
+
['circle', 'hexagon', 'pentagon', 'square', 'triangle',
|
|
807
|
+
'star8', 'star7', 'star6', 'star5, 'asterisk']
|
|
808
|
+
ptsize : int
|
|
809
|
+
Size of the point (should be zero if symbol:None).
|
|
810
|
+
outline : str or (R, G, B), optional
|
|
811
|
+
Line color of the symbol
|
|
812
|
+
fill : str or (R, G, B), optional
|
|
813
|
+
Filling color of the symbol
|
|
814
|
+
|
|
815
|
+
:Optional keyword arguments:
|
|
816
|
+
coord_ref : string
|
|
817
|
+
The interpretation of x,y in points_list:
|
|
818
|
+
'lonlat' (the default: x is degrees E, y is degrees N),
|
|
819
|
+
or 'image' (x is pixels right, y is pixels down).
|
|
820
|
+
If image coordinates are negative they are interpreted
|
|
821
|
+
relative to the end of the dimension like standard Python
|
|
822
|
+
indexing.
|
|
823
|
+
width : float
|
|
824
|
+
Line width of the symbol
|
|
825
|
+
outline_opacity : int, optional {0; 255}
|
|
826
|
+
Opacity of the line of the symbol.
|
|
827
|
+
fill_opacity : int, optional {0; 255}
|
|
828
|
+
Opacity of the filling of the symbol
|
|
829
|
+
box_outline : str or (R, G, B), optional
|
|
830
|
+
Line color of the textbox borders.
|
|
831
|
+
box_linewidth : float
|
|
832
|
+
Line width of the the borders of the textbox
|
|
833
|
+
box_fill : str or (R, G, B), optional
|
|
834
|
+
Filling color of the background of the textbox.
|
|
835
|
+
box_opacity : int, optional {0; 255}
|
|
836
|
+
Opacity of the background filling of the textbox.
|
|
837
|
+
"""
|
|
838
|
+
coord_converter = _CoordConverter(coord_ref, area_def)
|
|
839
|
+
draw = self._get_canvas(image)
|
|
840
|
+
|
|
841
|
+
# Iterate through points list
|
|
842
|
+
for point in points_list:
|
|
843
|
+
(x, y), desc = point
|
|
844
|
+
try:
|
|
845
|
+
x, y = coord_converter(x, y)
|
|
846
|
+
except ValueError:
|
|
847
|
+
logger.info(f"Point ({x}, {y}) is out of the image area, it will not be added to the image.")
|
|
848
|
+
continue
|
|
849
|
+
if ptsize != 0:
|
|
850
|
+
half_ptsize = int(round(ptsize / 2.0))
|
|
851
|
+
dot_box = [
|
|
852
|
+
x - half_ptsize,
|
|
853
|
+
y - half_ptsize,
|
|
854
|
+
x + half_ptsize,
|
|
855
|
+
y + half_ptsize,
|
|
856
|
+
]
|
|
857
|
+
|
|
858
|
+
width = kwargs.get("width", 1.0)
|
|
859
|
+
outline_opacity = kwargs.get("outline_opacity", 255)
|
|
860
|
+
fill_opacity = kwargs.get("fill_opacity", 255)
|
|
861
|
+
self._draw_point_element(
|
|
862
|
+
draw,
|
|
863
|
+
symbol,
|
|
864
|
+
dot_box,
|
|
865
|
+
x,
|
|
866
|
+
y,
|
|
867
|
+
width,
|
|
868
|
+
ptsize,
|
|
869
|
+
outline,
|
|
870
|
+
outline_opacity,
|
|
871
|
+
fill,
|
|
872
|
+
fill_opacity,
|
|
873
|
+
)
|
|
874
|
+
elif desc is None:
|
|
875
|
+
logger.error("'ptsize' is 0 and 'desc' is None, nothing will be added to the image.")
|
|
876
|
+
|
|
877
|
+
if desc is not None:
|
|
878
|
+
text_position = [
|
|
879
|
+
x + ptsize,
|
|
880
|
+
y,
|
|
881
|
+
] # draw the text box next to the point
|
|
882
|
+
font = self._get_font(outline, font_file, font_size)
|
|
883
|
+
|
|
884
|
+
new_kwargs = kwargs.copy()
|
|
860
885
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
dot_box = [x - ptsize, y - ptsize,
|
|
864
|
-
x + ptsize, y + ptsize]
|
|
865
|
-
self._draw_ellipse(
|
|
866
|
-
draw, dot_box, fill=outline, outline=outline)
|
|
867
|
-
text_position = [x + 9, y - 5] # FIX ME
|
|
868
|
-
else:
|
|
869
|
-
text_position = [x, y]
|
|
886
|
+
box_outline = new_kwargs.pop("box_outline", "white")
|
|
887
|
+
box_opacity = new_kwargs.pop("box_opacity", 0)
|
|
870
888
|
|
|
871
889
|
# add text_box
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
890
|
+
self._draw_text_box(
|
|
891
|
+
draw,
|
|
892
|
+
text_position,
|
|
893
|
+
desc,
|
|
894
|
+
font,
|
|
895
|
+
outline,
|
|
896
|
+
box_outline,
|
|
897
|
+
box_opacity,
|
|
898
|
+
**new_kwargs,
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
logger.debug("Point %s has been added to the image", str((x, y)))
|
|
875
902
|
|
|
876
903
|
self._finalize(draw)
|
|
877
904
|
|
|
905
|
+
def _draw_point_element(
|
|
906
|
+
self,
|
|
907
|
+
draw,
|
|
908
|
+
symbol,
|
|
909
|
+
dot_box,
|
|
910
|
+
x,
|
|
911
|
+
y,
|
|
912
|
+
width,
|
|
913
|
+
ptsize,
|
|
914
|
+
outline,
|
|
915
|
+
outline_opacity,
|
|
916
|
+
fill,
|
|
917
|
+
fill_opacity,
|
|
918
|
+
):
|
|
919
|
+
if symbol == "circle":
|
|
920
|
+
# a 'circle' or a 'dot' i.e. circle with fill
|
|
921
|
+
self._draw_ellipse(
|
|
922
|
+
draw,
|
|
923
|
+
dot_box,
|
|
924
|
+
outline=outline,
|
|
925
|
+
width=width,
|
|
926
|
+
outline_opacity=outline_opacity,
|
|
927
|
+
fill=fill,
|
|
928
|
+
fill_opacity=fill_opacity,
|
|
929
|
+
)
|
|
930
|
+
# All regular polygons are drawn horizontally based
|
|
931
|
+
elif symbol == "hexagon":
|
|
932
|
+
self.draw_hexagon(
|
|
933
|
+
draw,
|
|
934
|
+
x,
|
|
935
|
+
y,
|
|
936
|
+
ptsize,
|
|
937
|
+
outline=outline,
|
|
938
|
+
width=width,
|
|
939
|
+
outline_opacity=outline_opacity,
|
|
940
|
+
fill=fill,
|
|
941
|
+
fill_opacity=fill_opacity,
|
|
942
|
+
)
|
|
943
|
+
elif symbol == "pentagon":
|
|
944
|
+
self.draw_pentagon(
|
|
945
|
+
draw,
|
|
946
|
+
x,
|
|
947
|
+
y,
|
|
948
|
+
ptsize,
|
|
949
|
+
outline=outline,
|
|
950
|
+
width=width,
|
|
951
|
+
outline_opacity=outline_opacity,
|
|
952
|
+
fill=fill,
|
|
953
|
+
fill_opacity=fill_opacity,
|
|
954
|
+
)
|
|
955
|
+
elif symbol == "square":
|
|
956
|
+
self._draw_rectangle(
|
|
957
|
+
draw,
|
|
958
|
+
dot_box,
|
|
959
|
+
outline=outline,
|
|
960
|
+
width=width,
|
|
961
|
+
outline_opacity=outline_opacity,
|
|
962
|
+
fill=fill,
|
|
963
|
+
fill_opacity=fill_opacity,
|
|
964
|
+
)
|
|
965
|
+
elif symbol == "triangle":
|
|
966
|
+
self.draw_triangle(
|
|
967
|
+
draw,
|
|
968
|
+
x,
|
|
969
|
+
y,
|
|
970
|
+
ptsize,
|
|
971
|
+
outline=outline,
|
|
972
|
+
width=width,
|
|
973
|
+
outline_opacity=outline_opacity,
|
|
974
|
+
fill=fill,
|
|
975
|
+
fill_opacity=fill_opacity,
|
|
976
|
+
)
|
|
977
|
+
# All stars are drawn with one vertical ray on top
|
|
978
|
+
elif symbol in ["star8", "star7", "star6", "star5"]:
|
|
979
|
+
self.draw_star(
|
|
980
|
+
draw,
|
|
981
|
+
symbol,
|
|
982
|
+
x,
|
|
983
|
+
y,
|
|
984
|
+
ptsize,
|
|
985
|
+
outline=outline,
|
|
986
|
+
width=width,
|
|
987
|
+
outline_opacity=outline_opacity,
|
|
988
|
+
fill=fill,
|
|
989
|
+
fill_opacity=fill_opacity,
|
|
990
|
+
)
|
|
991
|
+
elif symbol == "asterisk": # an '*' sign
|
|
992
|
+
self._draw_asterisk(
|
|
993
|
+
draw,
|
|
994
|
+
ptsize,
|
|
995
|
+
(x, y),
|
|
996
|
+
outline=outline,
|
|
997
|
+
width=width,
|
|
998
|
+
outline_opacity=outline_opacity,
|
|
999
|
+
)
|
|
1000
|
+
elif symbol:
|
|
1001
|
+
raise ValueError("Unsupported symbol type: " + str(symbol))
|
|
878
1002
|
|
|
879
|
-
def _get_lon_lat_bounding_box(area_extent, x_size, y_size, prj):
|
|
880
|
-
"""Get extreme lon and lat values
|
|
881
|
-
"""
|
|
882
1003
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
lons_s2, lats_s2 = x_range, y_ur * np.ones(x_range.size)
|
|
890
|
-
lons_s3, lats_s3 = x_ur * np.ones(y_range.size), y_range
|
|
891
|
-
lons_s4, lats_s4 = x_range, y_ll * np.ones(x_range.size)
|
|
892
|
-
else:
|
|
893
|
-
lons_s1, lats_s1 = prj(np.ones(y_range.size) * x_ll, y_range,
|
|
894
|
-
inverse=True)
|
|
895
|
-
lons_s2, lats_s2 = prj(x_range, np.ones(x_range.size) * y_ur,
|
|
896
|
-
inverse=True)
|
|
897
|
-
lons_s3, lats_s3 = prj(np.ones(y_range.size) * x_ur, y_range,
|
|
898
|
-
inverse=True)
|
|
899
|
-
lons_s4, lats_s4 = prj(x_range, np.ones(x_range.size) * y_ll,
|
|
900
|
-
inverse=True)
|
|
901
|
-
|
|
902
|
-
angle_sum = 0
|
|
903
|
-
prev = None
|
|
904
|
-
for lon in np.concatenate((lons_s1, lons_s2,
|
|
905
|
-
lons_s3[::-1], lons_s4[::-1])):
|
|
906
|
-
if prev is not None:
|
|
907
|
-
delta = lon - prev
|
|
908
|
-
if abs(delta) > 180:
|
|
909
|
-
delta = (abs(delta) - 360) * np.sign(delta)
|
|
910
|
-
angle_sum += delta
|
|
911
|
-
prev = lon
|
|
1004
|
+
def _get_lon_lat_bounding_box(area_extent, x_size, y_size, prj):
|
|
1005
|
+
"""Get extreme lon and lat values."""
|
|
1006
|
+
bbox_lons, bbox_lats = _get_bounding_box_lonlat_sides(area_extent, x_size, y_size, prj)
|
|
1007
|
+
lons_s1, lons_s2, lons_s3, lons_s4 = bbox_lons
|
|
1008
|
+
lats_s1, lats_s2, lats_s3, lats_s4 = bbox_lats
|
|
1009
|
+
angle_sum = _get_angle_sum(lons_s1, lons_s2, lons_s3, lons_s4)
|
|
912
1010
|
|
|
913
1011
|
if round(angle_sum) == -360:
|
|
914
1012
|
# Covers NP
|
|
915
|
-
lat_min = min(lats_s1.min(), lats_s2.min(),
|
|
916
|
-
lats_s3.min(), lats_s4.min())
|
|
1013
|
+
lat_min = min(lats_s1.min(), lats_s2.min(), lats_s3.min(), lats_s4.min())
|
|
917
1014
|
lat_max = 90
|
|
918
1015
|
lon_min = -180
|
|
919
1016
|
lon_max = 180
|
|
920
1017
|
elif round(angle_sum) == 360:
|
|
921
1018
|
# Covers SP
|
|
922
1019
|
lat_min = -90
|
|
923
|
-
lat_max = max(lats_s1.max(), lats_s2.max(),
|
|
924
|
-
lats_s3.max(), lats_s4.max())
|
|
1020
|
+
lat_max = max(lats_s1.max(), lats_s2.max(), lats_s3.max(), lats_s4.max())
|
|
925
1021
|
lon_min = -180
|
|
926
1022
|
lon_max = 180
|
|
927
1023
|
elif round(angle_sum) == 0:
|
|
928
1024
|
# Covers no poles
|
|
929
|
-
if np.sign(lons_s1[0]) * np.sign(lons_s1[-1]) == -1:
|
|
1025
|
+
if np.sign(lons_s1[0]) * np.sign(lons_s1[-1]) == -1 and lons_s1.min() * lons_s1.max() < -25000:
|
|
930
1026
|
# End points of left side on different side of dateline
|
|
931
1027
|
lon_min = lons_s1[lons_s1 > 0].min()
|
|
932
1028
|
else:
|
|
933
1029
|
lon_min = lons_s1.min()
|
|
934
1030
|
|
|
935
|
-
if np.sign(lons_s3[0]) * np.sign(lons_s3[-1]) == -1:
|
|
1031
|
+
if np.sign(lons_s3[0]) * np.sign(lons_s3[-1]) == -1 and lons_s3.min() * lons_s3.max() < -25000:
|
|
936
1032
|
# End points of right side on different side of dateline
|
|
937
1033
|
lon_max = lons_s3[lons_s3 < 0].max()
|
|
938
1034
|
else:
|
|
@@ -947,6 +1043,7 @@ def _get_lon_lat_bounding_box(area_extent, x_size, y_size, prj):
|
|
|
947
1043
|
lon_min = -180
|
|
948
1044
|
lon_max = 180
|
|
949
1045
|
|
|
1046
|
+
# Catch inf/1e30 or other invalid values
|
|
950
1047
|
if not (-180 <= lon_min <= 180):
|
|
951
1048
|
lon_min = -180
|
|
952
1049
|
if not (-180 <= lon_max <= 180):
|
|
@@ -959,37 +1056,56 @@ def _get_lon_lat_bounding_box(area_extent, x_size, y_size, prj):
|
|
|
959
1056
|
return lon_min, lon_max, lat_min, lat_max
|
|
960
1057
|
|
|
961
1058
|
|
|
962
|
-
def
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1059
|
+
def _get_bounding_box_lonlat_sides(area_extent, x_size, y_size, prj):
|
|
1060
|
+
x_ll, y_ll, x_ur, y_ur = area_extent
|
|
1061
|
+
x_range = np.linspace(x_ll, x_ur, num=x_size)
|
|
1062
|
+
y_range = np.linspace(y_ll, y_ur, num=y_size)
|
|
966
1063
|
|
|
1064
|
+
lons_s1, lats_s1 = prj(np.ones(y_range.size) * x_ll, y_range, inverse=True)
|
|
1065
|
+
lons_s2, lats_s2 = prj(x_range, np.ones(x_range.size) * y_ur, inverse=True)
|
|
1066
|
+
lons_s3, lats_s3 = prj(np.ones(y_range.size) * x_ur, y_range, inverse=True)
|
|
1067
|
+
lons_s4, lats_s4 = prj(x_range, np.ones(x_range.size) * y_ll, inverse=True)
|
|
1068
|
+
return (lons_s1, lons_s2, lons_s3, lons_s4), (lats_s1, lats_s2, lats_s3, lats_s4)
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def _get_angle_sum(lons_s1, lons_s2, lons_s3, lons_s4):
|
|
1072
|
+
angle_sum = 0
|
|
1073
|
+
prev = None
|
|
1074
|
+
for lon in np.concatenate((lons_s1, lons_s2, lons_s3[::-1], lons_s4[::-1])):
|
|
1075
|
+
if not np.isfinite(lon):
|
|
1076
|
+
continue
|
|
1077
|
+
if prev is not None:
|
|
1078
|
+
delta = lon - prev
|
|
1079
|
+
if abs(delta) > 180:
|
|
1080
|
+
delta = (abs(delta) - 360) * np.sign(delta)
|
|
1081
|
+
angle_sum += delta
|
|
1082
|
+
prev = lon
|
|
1083
|
+
return angle_sum
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _get_pixel_index(shape, area_extent, x_size, y_size, prj, x_offset=0, y_offset=0):
|
|
1087
|
+
"""Map coordinates of shape to image coordinates."""
|
|
967
1088
|
# Get shape data as array and reproject
|
|
968
|
-
shape_data = np.array(shape.points)
|
|
1089
|
+
shape_data = np.array(shape.points if hasattr(shape, "points") else shape)
|
|
969
1090
|
lons = shape_data[:, 0]
|
|
970
1091
|
lats = shape_data[:, 1]
|
|
971
|
-
|
|
972
|
-
if prj.is_latlong():
|
|
973
|
-
x_ll, y_ll = prj(area_extent[0], area_extent[1])
|
|
974
|
-
x_ur, y_ur = prj(area_extent[2], area_extent[3])
|
|
975
|
-
else:
|
|
976
|
-
x_ll, y_ll, x_ur, y_ur = area_extent
|
|
1092
|
+
x_ll, y_ll, x_ur, y_ur = area_extent
|
|
977
1093
|
|
|
978
1094
|
x, y = prj(lons, lats)
|
|
979
1095
|
|
|
980
1096
|
# Handle out of bounds
|
|
981
1097
|
i = 0
|
|
982
1098
|
segments = []
|
|
983
|
-
if
|
|
1099
|
+
if (x >= 1e30).any() or (y >= 1e30).any():
|
|
984
1100
|
# Split polygon in line segments within projection
|
|
985
1101
|
is_reduced = True
|
|
986
|
-
if x[0]
|
|
1102
|
+
if x[0] >= 1e30 or y[0] >= 1e30:
|
|
987
1103
|
in_segment = False
|
|
988
1104
|
else:
|
|
989
1105
|
in_segment = True
|
|
990
1106
|
|
|
991
1107
|
for j in range(x.size):
|
|
992
|
-
if
|
|
1108
|
+
if x[j] >= 1e30 or y[j] >= 1e30:
|
|
993
1109
|
if in_segment:
|
|
994
1110
|
segments.append((x[i:j], y[i:j]))
|
|
995
1111
|
in_segment = False
|
|
@@ -1016,3 +1132,677 @@ def _get_pixel_index(shape, area_extent, x_size, y_size, prj,
|
|
|
1016
1132
|
index_arrays.append(index_array)
|
|
1017
1133
|
|
|
1018
1134
|
return index_arrays, is_reduced
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def iter_cities_names_lon_lat(
|
|
1138
|
+
cities_filename: str | Path, cities_list: list[str]
|
|
1139
|
+
) -> Generator[tuple[str, float, float], None, None]:
|
|
1140
|
+
"""Iterate over citiesN.txt files from GeoNames.org."""
|
|
1141
|
+
with open(cities_filename, mode="r", encoding="utf-8") as cities_file:
|
|
1142
|
+
for city_row in cities_file:
|
|
1143
|
+
city_info = city_row.split("\t")
|
|
1144
|
+
if not city_info or not (city_info[1] in cities_list or city_info[2] in cities_list):
|
|
1145
|
+
continue
|
|
1146
|
+
city_name, lon, lat = city_info[1], float(city_info[5]), float(city_info[4])
|
|
1147
|
+
yield city_name, lon, lat
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
class _OverlaysFromDict:
|
|
1151
|
+
"""Helper for drawing overlays from a dictionary of parameters."""
|
|
1152
|
+
|
|
1153
|
+
def __init__(self, contour_writer, overlays, area_def, cache_epoch, background):
|
|
1154
|
+
self._cw = contour_writer
|
|
1155
|
+
self._overlays = overlays
|
|
1156
|
+
self._is_cached = False
|
|
1157
|
+
self._cache_filename = None
|
|
1158
|
+
self._background = background
|
|
1159
|
+
self._area_def = area_def
|
|
1160
|
+
|
|
1161
|
+
foreground = None
|
|
1162
|
+
if "cache" in overlays:
|
|
1163
|
+
cache_filename, foreground = self._get_cached_filename_and_foreground(cache_epoch)
|
|
1164
|
+
self._cache_filename = cache_filename
|
|
1165
|
+
self._is_cached = foreground is not None
|
|
1166
|
+
|
|
1167
|
+
if foreground is None:
|
|
1168
|
+
if self._cache_filename is None and self._background is not None:
|
|
1169
|
+
foreground = self._background
|
|
1170
|
+
else:
|
|
1171
|
+
x_size = area_def.width
|
|
1172
|
+
y_size = area_def.height
|
|
1173
|
+
foreground = Image.new("RGBA", (x_size, y_size), (0, 0, 0, 0))
|
|
1174
|
+
|
|
1175
|
+
self._foreground = foreground
|
|
1176
|
+
|
|
1177
|
+
def _get_cached_filename_and_foreground(self, cache_epoch):
|
|
1178
|
+
cache_file = self._generate_cache_filename(
|
|
1179
|
+
self._overlays["cache"]["file"],
|
|
1180
|
+
self._area_def,
|
|
1181
|
+
self._overlays,
|
|
1182
|
+
)
|
|
1183
|
+
regenerate = self._overlays["cache"].get("regenerate", False)
|
|
1184
|
+
foreground = self._apply_cached_image(cache_file, cache_epoch, self._background, regenerate=regenerate)
|
|
1185
|
+
return cache_file, foreground
|
|
1186
|
+
|
|
1187
|
+
@staticmethod
|
|
1188
|
+
def _apply_cached_image(cache_file, cache_epoch, background, regenerate=False):
|
|
1189
|
+
try:
|
|
1190
|
+
config_time = cache_epoch or 0
|
|
1191
|
+
cache_time = os.path.getmtime(cache_file)
|
|
1192
|
+
# Cache file will be used only if it's newer than config file
|
|
1193
|
+
if config_time is not None and config_time < cache_time and not regenerate:
|
|
1194
|
+
foreground = Image.open(cache_file)
|
|
1195
|
+
logger.info("Using image in cache %s", cache_file)
|
|
1196
|
+
if background is not None:
|
|
1197
|
+
_apply_cached_foreground_on_background(background, foreground)
|
|
1198
|
+
return foreground
|
|
1199
|
+
logger.info("Regenerating cache file.")
|
|
1200
|
+
except OSError:
|
|
1201
|
+
logger.info("No overlay image found, new overlay image will be saved in cache.")
|
|
1202
|
+
return None
|
|
1203
|
+
|
|
1204
|
+
def _write_and_apply_new_cached_image(self):
|
|
1205
|
+
try:
|
|
1206
|
+
self._foreground.save(self._cache_filename)
|
|
1207
|
+
except IOError as e:
|
|
1208
|
+
logger.error("Can't save cache: %s", str(e))
|
|
1209
|
+
if self._background is not None:
|
|
1210
|
+
_apply_cached_foreground_on_background(self._background, self._foreground)
|
|
1211
|
+
|
|
1212
|
+
def _generate_cache_filename(self, cache_prefix, area_def, overlays_dict):
|
|
1213
|
+
area_hash = hash(area_def)
|
|
1214
|
+
base_dir, file_prefix = os.path.split(cache_prefix)
|
|
1215
|
+
params_to_hash = self._prepare_hashable_dict(overlays_dict)
|
|
1216
|
+
param_hash = hash_dict(params_to_hash)
|
|
1217
|
+
return os.path.join(base_dir, f"{file_prefix}_{area_hash}_{param_hash}.png")
|
|
1218
|
+
|
|
1219
|
+
@staticmethod
|
|
1220
|
+
def _prepare_hashable_dict(nonhashable_dict):
|
|
1221
|
+
params_to_hash = {}
|
|
1222
|
+
# avoid wasteful deep copy by only doing two levels of copying
|
|
1223
|
+
for overlay_name, overlay_dict in nonhashable_dict.items():
|
|
1224
|
+
if overlay_name == "cache":
|
|
1225
|
+
continue
|
|
1226
|
+
params_to_hash[overlay_name] = overlay_dict.copy()
|
|
1227
|
+
# font objects are not hashable
|
|
1228
|
+
for font_cat in ("cities", "points", "grid"):
|
|
1229
|
+
if font_cat in params_to_hash:
|
|
1230
|
+
params_to_hash[font_cat].pop("font", None)
|
|
1231
|
+
return params_to_hash
|
|
1232
|
+
|
|
1233
|
+
def apply_overlays(self):
|
|
1234
|
+
if self._is_cached:
|
|
1235
|
+
return self._foreground
|
|
1236
|
+
|
|
1237
|
+
overlays = self._overlays
|
|
1238
|
+
self._add_coasts_rivers_borders_from_dict(overlays)
|
|
1239
|
+
if "shapefiles" in overlays:
|
|
1240
|
+
self._add_shapefiles_from_dict(overlays["shapefiles"])
|
|
1241
|
+
if "grid" in overlays:
|
|
1242
|
+
self._add_grid_from_dict(overlays["grid"])
|
|
1243
|
+
if "cities" in overlays:
|
|
1244
|
+
self._add_cities_from_dict(overlays["cities"])
|
|
1245
|
+
for param_key in ["points", "text"]:
|
|
1246
|
+
if param_key not in overlays:
|
|
1247
|
+
continue
|
|
1248
|
+
self._add_points_from_dict(overlays[param_key])
|
|
1249
|
+
|
|
1250
|
+
if self._cache_filename is not None:
|
|
1251
|
+
self._write_and_apply_new_cached_image()
|
|
1252
|
+
return self._foreground
|
|
1253
|
+
|
|
1254
|
+
def _add_coasts_rivers_borders_from_dict(self, overlays):
|
|
1255
|
+
default_resolution = get_resolution_from_area(self._area_def)
|
|
1256
|
+
DEFAULT = {
|
|
1257
|
+
"level": 1,
|
|
1258
|
+
"outline": "white",
|
|
1259
|
+
"width": 1,
|
|
1260
|
+
"fill": None,
|
|
1261
|
+
"fill_opacity": 255,
|
|
1262
|
+
"outline_opacity": 255,
|
|
1263
|
+
"x_offset": 0,
|
|
1264
|
+
"y_offset": 0,
|
|
1265
|
+
"resolution": default_resolution,
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
for section, fun in zip(
|
|
1269
|
+
["coasts", "rivers", "borders"],
|
|
1270
|
+
[self._cw.add_coastlines, self._cw.add_rivers, self._cw.add_borders],
|
|
1271
|
+
):
|
|
1272
|
+
if section not in overlays:
|
|
1273
|
+
continue
|
|
1274
|
+
params = DEFAULT.copy()
|
|
1275
|
+
params.update(overlays[section])
|
|
1276
|
+
|
|
1277
|
+
if section != "coasts":
|
|
1278
|
+
params.pop("fill_opacity", None)
|
|
1279
|
+
params.pop("fill", None)
|
|
1280
|
+
|
|
1281
|
+
if not self._cw.is_agg:
|
|
1282
|
+
for key in ["width", "outline_opacity", "fill_opacity"]:
|
|
1283
|
+
params.pop(key, None)
|
|
1284
|
+
|
|
1285
|
+
fun(self._foreground, self._area_def, **params)
|
|
1286
|
+
logger.info("%s added", section.capitalize())
|
|
1287
|
+
|
|
1288
|
+
def _add_shapefiles_from_dict(self, shapefiles):
|
|
1289
|
+
# Backward compatibility and config.ini
|
|
1290
|
+
if isinstance(shapefiles, dict):
|
|
1291
|
+
shapefiles = [shapefiles]
|
|
1292
|
+
|
|
1293
|
+
DEFAULT_FILENAME = None
|
|
1294
|
+
DEFAULT_OUTLINE = "white"
|
|
1295
|
+
DEFAULT_FILL = None
|
|
1296
|
+
for params in shapefiles:
|
|
1297
|
+
params = params.copy() # don't modify the user's dictionary
|
|
1298
|
+
params.setdefault("filename", DEFAULT_FILENAME)
|
|
1299
|
+
params.setdefault("outline", DEFAULT_OUTLINE)
|
|
1300
|
+
params.setdefault("fill", DEFAULT_FILL)
|
|
1301
|
+
if not self._cw.is_agg:
|
|
1302
|
+
for key in ["width", "outline_opacity", "fill_opacity"]:
|
|
1303
|
+
params.pop(key, None)
|
|
1304
|
+
self._cw.add_shapefile_shapes(
|
|
1305
|
+
self._foreground,
|
|
1306
|
+
self._area_def,
|
|
1307
|
+
feature_type=None,
|
|
1308
|
+
x_offset=0,
|
|
1309
|
+
y_offset=0,
|
|
1310
|
+
**params,
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
def _add_grid_from_dict(self, grid_dict):
|
|
1314
|
+
if "major_lonlat" in grid_dict or "minor_lonlat" in grid_dict:
|
|
1315
|
+
Dlonlat = grid_dict.get("major_lonlat", (10.0, 10.0))
|
|
1316
|
+
dlonlat = grid_dict.get("minor_lonlat", (2.0, 2.0))
|
|
1317
|
+
else:
|
|
1318
|
+
Dlonlat = (
|
|
1319
|
+
grid_dict.get("lon_major", 10.0),
|
|
1320
|
+
grid_dict.get("lat_major", 10.0),
|
|
1321
|
+
)
|
|
1322
|
+
dlonlat = (
|
|
1323
|
+
grid_dict.get("lon_minor", 2.0),
|
|
1324
|
+
grid_dict.get("lat_minor", 2.0),
|
|
1325
|
+
)
|
|
1326
|
+
outline = grid_dict.get("outline", "white")
|
|
1327
|
+
write_text = grid_dict.get("write_text", True)
|
|
1328
|
+
if isinstance(write_text, str):
|
|
1329
|
+
write_text = write_text.lower() in ["true", "yes", "1", "on"]
|
|
1330
|
+
font = grid_dict.get("font", None)
|
|
1331
|
+
font_size = int(grid_dict.get("font_size", 10))
|
|
1332
|
+
fill = grid_dict.get("fill", outline)
|
|
1333
|
+
fill_opacity = grid_dict.get("fill_opacity", 255)
|
|
1334
|
+
if isinstance(font, str):
|
|
1335
|
+
if self._cw.is_agg:
|
|
1336
|
+
from aggdraw import Font
|
|
1337
|
+
|
|
1338
|
+
font = Font(fill, font, opacity=fill_opacity, size=font_size)
|
|
1339
|
+
else:
|
|
1340
|
+
from PIL.ImageFont import truetype
|
|
1341
|
+
|
|
1342
|
+
font = truetype(font, font_size)
|
|
1343
|
+
minor_outline = grid_dict.get("minor_outline", "white")
|
|
1344
|
+
minor_is_tick = grid_dict.get("minor_is_tick", True)
|
|
1345
|
+
if isinstance(minor_is_tick, str):
|
|
1346
|
+
minor_is_tick = minor_is_tick.lower() in ["true", "yes", "1"]
|
|
1347
|
+
lon_placement = grid_dict.get("lon_placement", "tb")
|
|
1348
|
+
lat_placement = grid_dict.get("lat_placement", "lr")
|
|
1349
|
+
|
|
1350
|
+
grid_kwargs = {}
|
|
1351
|
+
if self._cw.is_agg:
|
|
1352
|
+
width = float(grid_dict.get("width", 1.0))
|
|
1353
|
+
minor_width = float(grid_dict.get("minor_width", 0.5))
|
|
1354
|
+
outline_opacity = grid_dict.get("outline_opacity", 255)
|
|
1355
|
+
minor_outline_opacity = grid_dict.get("minor_outline_opacity", 255)
|
|
1356
|
+
grid_kwargs["width"] = width
|
|
1357
|
+
grid_kwargs["minor_width"] = minor_width
|
|
1358
|
+
grid_kwargs["outline_opacity"] = outline_opacity
|
|
1359
|
+
grid_kwargs["minor_outline_opacity"] = minor_outline_opacity
|
|
1360
|
+
|
|
1361
|
+
self._cw.add_grid(
|
|
1362
|
+
self._foreground,
|
|
1363
|
+
self._area_def,
|
|
1364
|
+
Dlonlat,
|
|
1365
|
+
dlonlat,
|
|
1366
|
+
font=font,
|
|
1367
|
+
write_text=write_text,
|
|
1368
|
+
fill=fill,
|
|
1369
|
+
outline=outline,
|
|
1370
|
+
minor_outline=minor_outline,
|
|
1371
|
+
minor_is_tick=minor_is_tick,
|
|
1372
|
+
lon_placement=lon_placement,
|
|
1373
|
+
lat_placement=lat_placement,
|
|
1374
|
+
**grid_kwargs,
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
def _add_cities_from_dict(self, cities_dict):
|
|
1378
|
+
# Backward compatibility and config.ini
|
|
1379
|
+
if isinstance(cities_dict, dict):
|
|
1380
|
+
cities_dict = [cities_dict]
|
|
1381
|
+
|
|
1382
|
+
DEFAULT_FONTSIZE = 12
|
|
1383
|
+
DEFAULT_SYMBOL = "circle"
|
|
1384
|
+
DEFAULT_PTSIZE = 6
|
|
1385
|
+
DEFAULT_OUTLINE = "black"
|
|
1386
|
+
DEFAULT_FILL = "white"
|
|
1387
|
+
|
|
1388
|
+
for params in cities_dict:
|
|
1389
|
+
params = params.copy()
|
|
1390
|
+
cities_list = params.pop("cities_list")
|
|
1391
|
+
font_file = params.pop("font")
|
|
1392
|
+
font_size = int(params.pop("font_size", DEFAULT_FONTSIZE))
|
|
1393
|
+
symbol = params.pop("symbol", DEFAULT_SYMBOL)
|
|
1394
|
+
ptsize = int(params.pop("ptsize", DEFAULT_PTSIZE))
|
|
1395
|
+
outline = params.pop("outline", DEFAULT_OUTLINE)
|
|
1396
|
+
fill = params.pop("fill", DEFAULT_FILL)
|
|
1397
|
+
|
|
1398
|
+
self._cw.add_cities(
|
|
1399
|
+
self._foreground,
|
|
1400
|
+
self._area_def,
|
|
1401
|
+
cities_list,
|
|
1402
|
+
font_file,
|
|
1403
|
+
font_size,
|
|
1404
|
+
symbol,
|
|
1405
|
+
ptsize,
|
|
1406
|
+
outline,
|
|
1407
|
+
fill,
|
|
1408
|
+
**params,
|
|
1409
|
+
)
|
|
1410
|
+
|
|
1411
|
+
def _add_points_from_dict(self, points_dict):
|
|
1412
|
+
DEFAULT_FONTSIZE = 12
|
|
1413
|
+
DEFAULT_SYMBOL = "circle"
|
|
1414
|
+
DEFAULT_PTSIZE = 6
|
|
1415
|
+
DEFAULT_OUTLINE = "black"
|
|
1416
|
+
DEFAULT_FILL = "white"
|
|
1417
|
+
|
|
1418
|
+
params = points_dict.copy()
|
|
1419
|
+
points_list = list(params.pop("points_list"))
|
|
1420
|
+
font_file = params.pop("font")
|
|
1421
|
+
font_size = int(params.pop("font_size", DEFAULT_FONTSIZE))
|
|
1422
|
+
symbol = params.pop("symbol", DEFAULT_SYMBOL)
|
|
1423
|
+
ptsize = int(params.pop("ptsize", DEFAULT_PTSIZE))
|
|
1424
|
+
outline = params.pop("outline", DEFAULT_OUTLINE)
|
|
1425
|
+
fill = params.pop("fill", DEFAULT_FILL)
|
|
1426
|
+
|
|
1427
|
+
self._cw.add_points(
|
|
1428
|
+
self._foreground,
|
|
1429
|
+
self._area_def,
|
|
1430
|
+
points_list,
|
|
1431
|
+
font_file,
|
|
1432
|
+
font_size,
|
|
1433
|
+
symbol,
|
|
1434
|
+
ptsize,
|
|
1435
|
+
outline,
|
|
1436
|
+
fill,
|
|
1437
|
+
**params,
|
|
1438
|
+
)
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
def _apply_cached_foreground_on_background(background, foreground):
|
|
1442
|
+
premult_foreground = foreground.convert("RGBa")
|
|
1443
|
+
if background.mode == "RGBA":
|
|
1444
|
+
# Cached foreground and background are both RGBA, no extra conversions needed
|
|
1445
|
+
background.paste(premult_foreground, mask=premult_foreground)
|
|
1446
|
+
return
|
|
1447
|
+
background_rgba = background.convert("RGBA")
|
|
1448
|
+
background_rgba.paste(premult_foreground, mask=premult_foreground)
|
|
1449
|
+
# overwrite background image in place
|
|
1450
|
+
background.paste(background_rgba)
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
class _GridDrawer:
|
|
1454
|
+
"""Helper for drawing graticule/grid lines."""
|
|
1455
|
+
|
|
1456
|
+
def __init__(
|
|
1457
|
+
self,
|
|
1458
|
+
contour_writer,
|
|
1459
|
+
draw,
|
|
1460
|
+
area_def,
|
|
1461
|
+
Dlon,
|
|
1462
|
+
Dlat,
|
|
1463
|
+
dlon,
|
|
1464
|
+
dlat,
|
|
1465
|
+
font,
|
|
1466
|
+
write_text,
|
|
1467
|
+
kwargs,
|
|
1468
|
+
):
|
|
1469
|
+
self._cw = contour_writer
|
|
1470
|
+
self._draw = draw
|
|
1471
|
+
|
|
1472
|
+
try:
|
|
1473
|
+
proj_def = area_def.crs if hasattr(area_def, "crs") else area_def.proj_dict
|
|
1474
|
+
area_extent = area_def.area_extent
|
|
1475
|
+
except AttributeError:
|
|
1476
|
+
proj_def = area_def[0]
|
|
1477
|
+
area_extent = area_def[1]
|
|
1478
|
+
self._proj_def = proj_def
|
|
1479
|
+
self._area_extent = area_extent
|
|
1480
|
+
self._prj = Proj(proj_def)
|
|
1481
|
+
|
|
1482
|
+
# use kwargs for major lines ... but reform for minor lines:
|
|
1483
|
+
minor_line_kwargs = kwargs.copy()
|
|
1484
|
+
minor_line_kwargs["outline"] = kwargs["minor_outline"]
|
|
1485
|
+
if self._cw.is_agg:
|
|
1486
|
+
minor_line_kwargs["outline_opacity"] = kwargs["minor_outline_opacity"]
|
|
1487
|
+
minor_line_kwargs["width"] = kwargs["minor_width"]
|
|
1488
|
+
|
|
1489
|
+
# PIL ImageDraw objects versus aggdraw Draw objects
|
|
1490
|
+
self._x_size = draw.im.size[0] if hasattr(draw, "im") else draw.size[0]
|
|
1491
|
+
self._y_size = draw.im.size[1] if hasattr(draw, "im") else draw.size[1]
|
|
1492
|
+
|
|
1493
|
+
# Calculate min and max lons and lats of interest
|
|
1494
|
+
lon_min, lon_max, lat_min, lat_max = _get_lon_lat_bounding_box(
|
|
1495
|
+
area_extent, self._x_size, self._y_size, self._prj
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
# Handle dateline crossing
|
|
1499
|
+
if lon_max < lon_min:
|
|
1500
|
+
lon_max = 360 + lon_max
|
|
1501
|
+
|
|
1502
|
+
# Draw lonlat grid lines ...
|
|
1503
|
+
# create adjustment of line lengths to avoid cluttered pole lines
|
|
1504
|
+
if lat_max == 90.0:
|
|
1505
|
+
shorten_max_lat = Dlat
|
|
1506
|
+
else:
|
|
1507
|
+
shorten_max_lat = 0.0
|
|
1508
|
+
|
|
1509
|
+
if lat_min == -90.0:
|
|
1510
|
+
increase_min_lat = Dlat
|
|
1511
|
+
else:
|
|
1512
|
+
increase_min_lat = 0.0
|
|
1513
|
+
|
|
1514
|
+
# major lon lines
|
|
1515
|
+
round_lon_min = lon_min - (lon_min % Dlon)
|
|
1516
|
+
maj_lons = np.arange(round_lon_min, lon_max, Dlon)
|
|
1517
|
+
maj_lons[maj_lons > 180] = maj_lons[maj_lons > 180] - 360
|
|
1518
|
+
|
|
1519
|
+
# minor lon lines (ticks)
|
|
1520
|
+
min_lons = np.arange(round_lon_min, lon_max, dlon)
|
|
1521
|
+
min_lons[min_lons > 180] = min_lons[min_lons > 180] - 360
|
|
1522
|
+
|
|
1523
|
+
# Get min_lons not in maj_lons
|
|
1524
|
+
min_lons = np.setdiff1d(min_lons, maj_lons)
|
|
1525
|
+
|
|
1526
|
+
# lats along major lon lines
|
|
1527
|
+
lin_lats = np.arange(
|
|
1528
|
+
lat_min + increase_min_lat,
|
|
1529
|
+
lat_max - shorten_max_lat,
|
|
1530
|
+
float(lat_max - lat_min) / self._y_size,
|
|
1531
|
+
)
|
|
1532
|
+
# lin_lats in rather high definition so that it can be used to
|
|
1533
|
+
# position text labels near edges of image...
|
|
1534
|
+
|
|
1535
|
+
# perhaps better to find the actual length of line in pixels...
|
|
1536
|
+
round_lat_min = lat_min - (lat_min % Dlat)
|
|
1537
|
+
|
|
1538
|
+
# major lat lines
|
|
1539
|
+
maj_lats = np.arange(round_lat_min + increase_min_lat, lat_max, Dlat)
|
|
1540
|
+
|
|
1541
|
+
# minor lon lines (ticks)
|
|
1542
|
+
min_lats = np.arange(round_lat_min + increase_min_lat, lat_max - shorten_max_lat, dlat)
|
|
1543
|
+
|
|
1544
|
+
# Get min_lats not in maj_lats
|
|
1545
|
+
min_lats = np.setdiff1d(min_lats, maj_lats)
|
|
1546
|
+
|
|
1547
|
+
# lons along major lat lines (extended slightly to avoid missing the end)
|
|
1548
|
+
lin_lons = np.linspace(lon_min, lon_max + Dlon / 5.0, max(self._x_size, self._y_size) // 5)
|
|
1549
|
+
|
|
1550
|
+
self._min_lons = min_lons
|
|
1551
|
+
self._min_lats = min_lats
|
|
1552
|
+
self._lin_lons = lin_lons
|
|
1553
|
+
self._lin_lats = lin_lats
|
|
1554
|
+
self._maj_lons = maj_lons
|
|
1555
|
+
self._maj_lats = maj_lats
|
|
1556
|
+
self._kwargs = kwargs
|
|
1557
|
+
self._minor_line_kwargs = minor_line_kwargs
|
|
1558
|
+
self._x_offset = 0
|
|
1559
|
+
self._y_offset = 0
|
|
1560
|
+
# text margins (at sides of image frame)
|
|
1561
|
+
self._y_text_margin = 4
|
|
1562
|
+
self._x_text_margin = 4
|
|
1563
|
+
self._write_text = write_text
|
|
1564
|
+
self._font = font
|
|
1565
|
+
self._Dlon = Dlon
|
|
1566
|
+
self._Dlat = Dlat
|
|
1567
|
+
self._dlon = dlon
|
|
1568
|
+
self._dlat = dlat
|
|
1569
|
+
self._lat_max = lat_max
|
|
1570
|
+
self._lat_min = lat_min
|
|
1571
|
+
|
|
1572
|
+
def draw_grid(self):
|
|
1573
|
+
if self._kwargs["minor_is_tick"]:
|
|
1574
|
+
self._draw_minor_ticks()
|
|
1575
|
+
else:
|
|
1576
|
+
self._draw_minor_lines()
|
|
1577
|
+
self._draw_major_lon_lines()
|
|
1578
|
+
self._draw_major_lat_lines()
|
|
1579
|
+
self._draw_pole_crosses()
|
|
1580
|
+
|
|
1581
|
+
def _draw_minor_lines(self):
|
|
1582
|
+
minor_lat_lines = self._get_minor_lat_lines()
|
|
1583
|
+
minor_lon_lines = self._get_minor_lon_lines()
|
|
1584
|
+
self._draw_minor_grid_lines(
|
|
1585
|
+
minor_lat_lines + minor_lon_lines,
|
|
1586
|
+
self._minor_line_kwargs,
|
|
1587
|
+
)
|
|
1588
|
+
|
|
1589
|
+
def _get_minor_lat_lines(self):
|
|
1590
|
+
return [[(x, lat) for x in self._lin_lons] for lat in self._min_lats]
|
|
1591
|
+
|
|
1592
|
+
def _get_minor_lon_lines(self):
|
|
1593
|
+
return [[(lon, x) for x in self._lin_lats] for lon in self._min_lons]
|
|
1594
|
+
|
|
1595
|
+
def _draw_minor_ticks(self):
|
|
1596
|
+
# minor tick lines on major lines
|
|
1597
|
+
# Draw 'minor' tick lines dlat separation along the lon
|
|
1598
|
+
minor_tick_lon_lines = self._get_minor_lon_tick_lines()
|
|
1599
|
+
# Draw 'minor' tick dlon separation along the lat
|
|
1600
|
+
minor_tick_lat_lines = self._get_minor_lat_tick_lines()
|
|
1601
|
+
self._draw_minor_grid_lines(
|
|
1602
|
+
minor_tick_lon_lines + minor_tick_lat_lines,
|
|
1603
|
+
self._minor_line_kwargs,
|
|
1604
|
+
)
|
|
1605
|
+
|
|
1606
|
+
def _get_minor_lon_tick_lines(self):
|
|
1607
|
+
minor_tick_lon_lines = []
|
|
1608
|
+
for lon in self._maj_lons:
|
|
1609
|
+
tick_lons = np.linspace(lon - self._Dlon / 20.0, lon + self._Dlon / 20.0, 5)
|
|
1610
|
+
minor_tick_lines = [[(x, lat) for x in tick_lons] for lat in self._min_lats]
|
|
1611
|
+
minor_tick_lon_lines.extend(minor_tick_lines)
|
|
1612
|
+
return minor_tick_lon_lines
|
|
1613
|
+
|
|
1614
|
+
def _get_minor_lat_tick_lines(self):
|
|
1615
|
+
minor_tick_lat_lines = []
|
|
1616
|
+
for lat in self._maj_lats:
|
|
1617
|
+
tick_lats = np.linspace(lat - self._Dlat / 20.0, lat + self._Dlat / 20.0, 5)
|
|
1618
|
+
minor_tick_lines = [[(lon, x) for x in tick_lats] for lon in self._min_lons]
|
|
1619
|
+
minor_tick_lat_lines.extend(minor_tick_lines)
|
|
1620
|
+
return minor_tick_lat_lines
|
|
1621
|
+
|
|
1622
|
+
def _draw_minor_grid_lines(
|
|
1623
|
+
self,
|
|
1624
|
+
minor_lines,
|
|
1625
|
+
kwargs,
|
|
1626
|
+
):
|
|
1627
|
+
index_arrays = self._grid_line_index_array_generator(minor_lines)
|
|
1628
|
+
for index_array in index_arrays:
|
|
1629
|
+
self._cw._draw_line(self._draw, index_array.flatten().tolist(), **kwargs)
|
|
1630
|
+
|
|
1631
|
+
def _draw_major_lon_lines(self):
|
|
1632
|
+
def _lon_label_from_line_lonlats(lonlats):
|
|
1633
|
+
return self._grid_lon_label(lonlats[0][0])
|
|
1634
|
+
|
|
1635
|
+
major_lon_lines = [[(lon, x) for x in self._lin_lats] for lon in self._maj_lons]
|
|
1636
|
+
label_placement = self._kwargs["lon_placement"].lower()
|
|
1637
|
+
self._draw_major_lines(
|
|
1638
|
+
major_lon_lines,
|
|
1639
|
+
_lon_label_from_line_lonlats,
|
|
1640
|
+
label_placement,
|
|
1641
|
+
)
|
|
1642
|
+
|
|
1643
|
+
def _draw_major_lat_lines(self):
|
|
1644
|
+
def _lat_label_from_line_lonlats(lonlats):
|
|
1645
|
+
return self._grid_lat_label(lonlats[0][1])
|
|
1646
|
+
|
|
1647
|
+
major_lat_lines = [[(x, lat) for x in self._lin_lons] for lat in self._maj_lats]
|
|
1648
|
+
label_placement = self._kwargs["lat_placement"].lower()
|
|
1649
|
+
self._draw_major_lines(
|
|
1650
|
+
major_lat_lines,
|
|
1651
|
+
_lat_label_from_line_lonlats,
|
|
1652
|
+
label_placement,
|
|
1653
|
+
)
|
|
1654
|
+
|
|
1655
|
+
def _draw_major_lines(self, major_lines_lonlats, label_gen_func, label_placement_definition):
|
|
1656
|
+
for lonlats in major_lines_lonlats:
|
|
1657
|
+
index_arrays = self._grid_line_index_array_generator(
|
|
1658
|
+
[lonlats],
|
|
1659
|
+
)
|
|
1660
|
+
index_array = None
|
|
1661
|
+
for index_array in index_arrays:
|
|
1662
|
+
self._cw._draw_line(self._draw, index_array.flatten().tolist(), **self._kwargs)
|
|
1663
|
+
|
|
1664
|
+
# add lon text markings at each end of longitude line
|
|
1665
|
+
if self._write_text and index_array is not None:
|
|
1666
|
+
txt = label_gen_func(lonlats)
|
|
1667
|
+
xys = _find_line_intercepts(
|
|
1668
|
+
index_array,
|
|
1669
|
+
(self._x_size, self._y_size),
|
|
1670
|
+
(self._x_text_margin, self._y_text_margin),
|
|
1671
|
+
)
|
|
1672
|
+
|
|
1673
|
+
self._draw_grid_labels(
|
|
1674
|
+
self._draw,
|
|
1675
|
+
xys,
|
|
1676
|
+
label_placement_definition,
|
|
1677
|
+
txt,
|
|
1678
|
+
self._font,
|
|
1679
|
+
**self._kwargs,
|
|
1680
|
+
)
|
|
1681
|
+
|
|
1682
|
+
def _grid_line_index_array_generator(
|
|
1683
|
+
self,
|
|
1684
|
+
grid_lines,
|
|
1685
|
+
):
|
|
1686
|
+
for grid_line_lonlats in grid_lines:
|
|
1687
|
+
index_arrays, is_reduced = _get_pixel_index(
|
|
1688
|
+
grid_line_lonlats,
|
|
1689
|
+
self._area_extent,
|
|
1690
|
+
self._x_size,
|
|
1691
|
+
self._y_size,
|
|
1692
|
+
self._prj,
|
|
1693
|
+
x_offset=self._x_offset,
|
|
1694
|
+
y_offset=self._y_offset,
|
|
1695
|
+
)
|
|
1696
|
+
# Skip empty datasets
|
|
1697
|
+
if not index_arrays:
|
|
1698
|
+
continue
|
|
1699
|
+
yield from index_arrays
|
|
1700
|
+
|
|
1701
|
+
def _grid_lon_label(self, lon):
|
|
1702
|
+
# FIXME: Use f-strings or just pass the direction
|
|
1703
|
+
if lon > 0.0:
|
|
1704
|
+
txt = "%.2dE" % (lon)
|
|
1705
|
+
else:
|
|
1706
|
+
txt = "%.2dW" % (-lon)
|
|
1707
|
+
return txt
|
|
1708
|
+
|
|
1709
|
+
def _grid_lat_label(self, lat):
|
|
1710
|
+
if lat >= 0.0:
|
|
1711
|
+
txt = "%.2dN" % (lat)
|
|
1712
|
+
else:
|
|
1713
|
+
txt = "%.2dS" % (-lat)
|
|
1714
|
+
return txt
|
|
1715
|
+
|
|
1716
|
+
def _draw_grid_labels(self, draw, xys, placement_def, txt, font, **kwargs):
|
|
1717
|
+
"""Draw text with default PIL module."""
|
|
1718
|
+
if font is None:
|
|
1719
|
+
# NOTE: Default font does not use font size in PIL writer
|
|
1720
|
+
font = self._cw._get_font(kwargs.get("fill", "black"), font, 12)
|
|
1721
|
+
for xy in xys:
|
|
1722
|
+
# note xy[0] is xy coordinate pair,
|
|
1723
|
+
# xy[1] is required alignment e.g. 'tl','lr','lc','cc'...
|
|
1724
|
+
ax, ay = xy[1].lower()
|
|
1725
|
+
if ax in placement_def or ay in placement_def:
|
|
1726
|
+
self._cw._draw_text(draw, xy[0], txt, font, align=xy[1], **kwargs)
|
|
1727
|
+
|
|
1728
|
+
def _draw_pole_crosses(self):
|
|
1729
|
+
cross_lats_interval = float(self._lat_max - self._lat_min) / self._y_size
|
|
1730
|
+
if self._lat_max == 90.0:
|
|
1731
|
+
crosslats = np.arange(
|
|
1732
|
+
90.0 - self._Dlat / 2.0,
|
|
1733
|
+
90.0,
|
|
1734
|
+
cross_lats_interval,
|
|
1735
|
+
)
|
|
1736
|
+
self._add_pole_crosslats(crosslats)
|
|
1737
|
+
|
|
1738
|
+
if self._lat_min == -90.0:
|
|
1739
|
+
crosslats = np.arange(-90.0, -90.0 + self._Dlat / 2.0, cross_lats_interval)
|
|
1740
|
+
self._add_pole_crosslats(crosslats)
|
|
1741
|
+
|
|
1742
|
+
def _add_pole_crosslats(self, crosslats):
|
|
1743
|
+
cross_lines = [[(lon, x) for x in crosslats] for lon in (0.0, 90.0, 180.0, -90.0)]
|
|
1744
|
+
self._draw_minor_grid_lines(
|
|
1745
|
+
cross_lines,
|
|
1746
|
+
self._kwargs,
|
|
1747
|
+
)
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
def _find_line_intercepts(xys, size, margins):
|
|
1751
|
+
"""Find intercepts of poly-line xys with image boundaries offset by margins.
|
|
1752
|
+
|
|
1753
|
+
Returns an array of coordinates.
|
|
1754
|
+
|
|
1755
|
+
"""
|
|
1756
|
+
x_size, y_size = size
|
|
1757
|
+
|
|
1758
|
+
def is_in_box(x_y, extents):
|
|
1759
|
+
x, y = x_y
|
|
1760
|
+
xmin, xmax, ymin, ymax = extents
|
|
1761
|
+
return xmin < x < xmax and ymin < y < ymax
|
|
1762
|
+
|
|
1763
|
+
def crossing(x1, x2, lim):
|
|
1764
|
+
return (x1 < lim) != (x2 < lim)
|
|
1765
|
+
|
|
1766
|
+
# set box limits
|
|
1767
|
+
xlim1 = margins[0]
|
|
1768
|
+
ylim1 = margins[1]
|
|
1769
|
+
xlim2 = x_size - margins[0]
|
|
1770
|
+
ylim2 = y_size - margins[1]
|
|
1771
|
+
|
|
1772
|
+
# only consider crossing within a box a little bigger than grid boundary
|
|
1773
|
+
search_box = (-10, x_size + 10, -10, y_size + 10)
|
|
1774
|
+
|
|
1775
|
+
# loop through line steps and detect crossings
|
|
1776
|
+
intercepts = []
|
|
1777
|
+
align_left = "LC"
|
|
1778
|
+
align_right = "RC"
|
|
1779
|
+
align_top = "CT"
|
|
1780
|
+
align_bottom = "CB"
|
|
1781
|
+
prev_xy = xys[0]
|
|
1782
|
+
for xy in xys[1:]:
|
|
1783
|
+
if not is_in_box(xy, search_box):
|
|
1784
|
+
prev_xy = xy
|
|
1785
|
+
continue
|
|
1786
|
+
# crossing LHS
|
|
1787
|
+
if crossing(prev_xy[0], xy[0], xlim1):
|
|
1788
|
+
x = xlim1
|
|
1789
|
+
y = xy[1]
|
|
1790
|
+
intercepts.append(((x, y), align_left))
|
|
1791
|
+
# crossing RHS
|
|
1792
|
+
elif crossing(prev_xy[0], xy[0], xlim2):
|
|
1793
|
+
x = xlim2
|
|
1794
|
+
y = xy[1]
|
|
1795
|
+
intercepts.append(((x, y), align_right))
|
|
1796
|
+
# crossing Top
|
|
1797
|
+
elif crossing(prev_xy[1], xy[1], ylim1):
|
|
1798
|
+
x = xy[0]
|
|
1799
|
+
y = ylim1
|
|
1800
|
+
intercepts.append(((x, y), align_top))
|
|
1801
|
+
# crossing Bottom
|
|
1802
|
+
elif crossing(prev_xy[1], xy[1], ylim2):
|
|
1803
|
+
x = xy[0] # - txt_width/2
|
|
1804
|
+
y = ylim2 # - txt_height
|
|
1805
|
+
intercepts.append(((x, y), align_bottom))
|
|
1806
|
+
prev_xy = xy
|
|
1807
|
+
|
|
1808
|
+
return intercepts
|