get_coordinates_by_zipcode.py 3.96 KB
Newer Older
1
2
3
4
5
6
7
"""
For a given German Zipcode, returns the corresponding WKT Polygon or Multipolygon.
If pyperclip is installed, the WKT gets copied to the clipboard,
e.g. for RegionChooser or download_files_from_LGL_BW.py.

Also accepts multiple Zipcodes, or Zipcode prefix.

8
    usage: get_coordinates_by_zipcode.py [-h] [-p PRECISION] PLZ [PLZ ...]
9
10
11
12

    Get WKT geometry for desired PLZs

    positional arguments:
13
    PLZ                   desired PLZs
14
15

    options:
16
17
18
19
    -h, --help            show this help message and exit
    -p PRECISION, --precision PRECISION
                            precision of returned polygon [m]

20
21
22

> python get_coordinates_by_zipcode.py 70174
> python get_coordinates_by_zipcode.py 70567 70569
23
> python get_coordinates_by_zipcode.py -p1000 70
24
25
"""

Eric Duminil's avatar
TODOs    
Eric Duminil committed
26
27
28
# TODO: Write tests
# TODO: Rename functions

29
30
31
32
33
34
35
36
import argparse
import json
import re
from pathlib import Path
from shapely.geometry import shape
from shapely.ops import unary_union

INPUT_FOLDER = Path('plz')
37
PLZ_FILENAME: str = 'plz-5stellig.geojson'
Eric Duminil's avatar
Eric Duminil committed
38
PLZ_SHAPE_FILE = INPUT_FOLDER / PLZ_FILENAME
39
40
PRECISION: float = 10  # [m]
ONE_DEGREE: float = 40e6 / 360  # [m]
Eric Duminil's avatar
Eric Duminil committed
41

42
43
PLZ_SHAPES: dict
CACHED: bool = False
44
45


46
def _download_plz_shapes_if_needed() -> None:
Eric Duminil's avatar
Eric Duminil committed
47
    if not PLZ_SHAPE_FILE.exists():
48
49
        from tqdm import tqdm
        import requests
Eric Duminil's avatar
Eric Duminil committed
50
        print("Downloading %s..." % PLZ_FILENAME)
51
52
53
        URL = "https://downloads.suche-postleitzahl.org/v2/public/" + PLZ_FILENAME
        response = requests.get(URL, stream=True)
        INPUT_FOLDER.mkdir(exist_ok=True)
Eric Duminil's avatar
Eric Duminil committed
54
        with open(PLZ_SHAPE_FILE, "wb") as handle:
55
56
            for data in tqdm(response.iter_content(chunk_size=1024), unit='kB'):
                handle.write(data)
Eric Duminil's avatar
Eric Duminil committed
57
        print('  Done')
58
59


60
61
62
def _get_plz_shapes() -> dict:
    global PLZ_SHAPES, CACHED
    if CACHED:
Eric Duminil's avatar
Eric Duminil committed
63
64
        return PLZ_SHAPES

Eric Duminil's avatar
Eric Duminil committed
65
    _download_plz_shapes_if_needed()
66
    try:
Eric Duminil's avatar
Eric Duminil committed
67
        print("Parsing %s..." % PLZ_FILENAME)
Eric Duminil's avatar
Eric Duminil committed
68
        with open(PLZ_SHAPE_FILE) as f:
69
            print('  Done')
Eric Duminil's avatar
Eric Duminil committed
70
            PLZ_SHAPES = json.load(f)
71
            CACHED = True
Eric Duminil's avatar
Eric Duminil committed
72
            return PLZ_SHAPES
73
    except json.decoder.JSONDecodeError:
Eric Duminil's avatar
Eric Duminil committed
74
        PLZ_SHAPE_FILE.unlink()
75
76
77
        raise AttributeError(f"{PLZ_FILENAME} seems to be damaged. Removing it. Please try again!")


78
def get_coordinates_by_zipcode(plz_patterns: list[str], precision: float = PRECISION) -> str:
Eric Duminil's avatar
Eric Duminil committed
79
    plz_shapes = _get_plz_shapes()
80
81
82
    geometries = []
    for plz_pattern in plz_patterns:
        found = False
Eric Duminil's avatar
Eric Duminil committed
83
        for plz_geojson in plz_shapes['features']:
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
            if re.match(plz_pattern, plz_geojson['properties']['plz']):
                found = True

                properties = plz_geojson['properties']

                print('## %s' % properties['note'])
                print('Population : %d' % properties['einwohner'])
                print('Area : %.2f km²' % properties['qkm'])
                # NOTE : Geometry can be either a polygon,
                # a MultiPolygon : 98694 Ilmenau
                # or a polygon with holes : 31860 Emmerthal
                print('WKT Polygon : ')

                geometries.append(shape(plz_geojson['geometry']))

        if not found:
            raise AttributeError(f"Sorry, no information could be found for PLZ={plz_pattern}")

    merged = unary_union(geometries)
Eric Duminil's avatar
Eric Duminil committed
103
    wkt_polygon = merged.simplify(precision / ONE_DEGREE).wkt
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
    print(wkt_polygon)
    try:
        import pyperclip
        pyperclip.copy(wkt_polygon)
        print("WKT Polygon copied to clipboard.")
    except ModuleNotFoundError:
        pass

    print()
    print("Done!")
    return wkt_polygon


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Get WKT geometry for desired PLZs')
    parser.add_argument('plzs', metavar='PLZ', type=str, nargs='+',
                        help='desired PLZs')
121
122
    parser.add_argument('-p', '--precision', default=PRECISION, type=int,
                        help='precision of returned polygon [m]')
123
    args = parser.parse_args()
124
    get_coordinates_by_zipcode(args.plzs, args.precision)